From 70f4d2cceda78379147aacbdc078a7fb1ce5ae6b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 2 Jun 2026 12:38:39 -0400 Subject: [PATCH 1/5] feat(callbacks): add Community Apps launch action - Add a typed communityApps action for Unraid OS to launch Community Apps inside an iframe. - Before, callback payloads only modeled account, license, and OS update actions. - That left CA iframe launches without a shared contract for server context, install targets, or theme/path preferences. - Add CommunityAppsLaunch to the UPC action union with server data and optional install URL customization fields. - Export the new action type through client and server entrypoints and cover server round-trip generation/parsing. - Regenerate package declaration files so consumers can type against the new contract. --- README.md | 1 + dist/client.d.ts | 4 ++-- dist/server.d.ts | 4 ++-- dist/types.d.ts | 14 +++++++++++++- src/__tests__/server.test.ts | 36 +++++++++++++++++++++++++++++++++++- src/client.ts | 4 ++++ src/server.ts | 4 ++++ src/types.ts | 15 ++++++++++++++- 8 files changed, 75 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 82185e6..d694ab7 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ const decrypted = callback.watcher(); - `SignIn`, `SignOut`, `OemSignOut`, etc. - Various callback action types - `ServerData` - Server information structure - `UserInfo` - User information structure +- `CommunityAppsLaunch` - Unraid OS to Community Apps iframe launch action - `ExternalActions` - Union type of all external actions - `UpcActions` - Union type of all UPC actions - `QueryPayloads` - Union type of all payload types diff --git a/dist/client.d.ts b/dist/client.d.ts index f03b7d2..ea4f7cd 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; export declare const createCallback: (config: CallbackConfig) => { send: (url: string, payload: SendPayloads, redirectType?: "newTab" | "replace" | null, sendType?: string, sender?: string) => void; parse: (data: string, options?: { @@ -19,4 +19,4 @@ export declare const useCallback: (config: CallbackConfig) => { watcher: (options?: WatcherOptions) => QueryPayloads | undefined; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/server.d.ts b/dist/server.d.ts index 3266842..7708498 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; /** * Server-safe factory that exposes only parse and generateUrl. * @@ -11,4 +11,4 @@ export declare const createServerCallback: (config: CallbackConfig) => { }) => QueryPayloads; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/types.d.ts b/dist/types.d.ts index 41c313a..a6c5fb5 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -16,6 +16,7 @@ export type Manage = "manage"; export type MyKeys = "myKeys"; export type LinkKey = "linkKey"; export type Activate = "activate"; +export type CommunityApps = "communityApps"; export type AccountActionTypes = Troubleshoot | SignIn | SignOut | OemSignOut | Manage | MyKeys | LinkKey; export type AccountKeyActionTypes = Recover | Replace | TrialExtend | TrialStart | UpdateOs | DowngradeOs; export type PurchaseActionTypes = Purchase | Redeem | Renew | Upgrade | Activate; @@ -94,8 +95,19 @@ export interface ServerTroubleshoot { type: Troubleshoot; server: ServerData; } +export interface CommunityAppsLaunch { + type: CommunityApps; + server: ServerData; + installUrl?: string; + installUrlTemplate?: string; + installParam?: string; + installTarget?: string; + path?: string; + theme?: string; + locale?: string; +} export type ExternalActions = ExternalSignIn | ExternalSignOut | ExternalKeyActions | ExternalUpdateOsAction; -export type UpcActions = ServerPayload | ServerTroubleshoot; +export type UpcActions = ServerPayload | ServerTroubleshoot | CommunityAppsLaunch; export type SendPayloads = ExternalActions[] | UpcActions[]; export interface ExternalPayload { type: "forUpc"; diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 65102aa..5bf17de 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { createServerCallback } from '../server' -import type { ServerPayload } from '../types' +import type { CommunityAppsLaunch, ServerPayload } from '../types' describe('createServerCallback (server entry)', () => { const config = { @@ -68,4 +68,38 @@ describe('createServerCallback (server entry)', () => { type: 'forUpc', }) }) + + it('should round-trip a Community Apps launch action', () => { + const { parse, generateUrl } = createServerCallback(config) + const testActions: CommunityAppsLaunch[] = [ + { + type: 'communityApps', + server: { + connectPluginVersion: '2024.05.06.1049', + connectState: 'CONNECTED', + guid: 'test-guid', + name: 'Tower', + osVersion: '7.2.0', + registered: true, + state: 'STARTER', + }, + installUrlTemplate: '/Apps/AddContainer?xmlTemplate={templateUrl}', + installTarget: '_top', + path: '/apps', + theme: 'dark', + }, + ] + + const generatedUrl = generateUrl('https://ca.unraid.net/apps', testActions, 'fromUpc', 'https://tower.local/redirect?target=%2Fapps') + const url = new URL(generatedUrl) + const encryptedData = url.hash.startsWith('#data=') + ? url.hash.slice('#data='.length) + : url.searchParams.get('data') || '' + + expect(parse(encryptedData)).toEqual({ + actions: testActions, + sender: 'https://tower.local/redirect?target=%2Fapps', + type: 'fromUpc', + }) + }) }) diff --git a/src/client.ts b/src/client.ts index c3553bb..f8a61ca 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,6 +21,7 @@ import type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -35,6 +36,7 @@ import type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, @@ -206,6 +208,7 @@ export type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -220,6 +223,7 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, diff --git a/src/server.ts b/src/server.ts index b76e95f..619a805 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,6 +20,7 @@ import type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -34,6 +35,7 @@ import type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, @@ -106,6 +108,7 @@ export type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -120,6 +123,7 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, diff --git a/src/types.ts b/src/types.ts index 9e64557..dbea798 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export type Manage = "manage"; export type MyKeys = "myKeys"; export type LinkKey = "linkKey"; export type Activate = "activate"; +export type CommunityApps = "communityApps"; export type AccountActionTypes = | Troubleshoot | SignIn @@ -161,13 +162,25 @@ export interface ServerTroubleshoot { server: ServerData; } +export interface CommunityAppsLaunch { + type: CommunityApps; + server: ServerData; + installUrl?: string; + installUrlTemplate?: string; + installParam?: string; + installTarget?: string; + path?: string; + theme?: string; + locale?: string; +} + export type ExternalActions = | ExternalSignIn | ExternalSignOut | ExternalKeyActions | ExternalUpdateOsAction; -export type UpcActions = ServerPayload | ServerTroubleshoot; +export type UpcActions = ServerPayload | ServerTroubleshoot | CommunityAppsLaunch; export type SendPayloads = ExternalActions[] | UpcActions[]; From 74dffa60f50812695120db691f6cde61a4a9240b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 2 Jun 2026 13:54:34 -0400 Subject: [PATCH 2/5] feat(callbacks): require opt-in installed app fingerprints - Add an explicit installedApps payload block for Community Apps launches. - Before, the launch action had no contract for installed-state exposure. - That made it unclear how Core should represent optional installed app matching without weakening the privacy boundary. - Model installed app fingerprints as an optional enabled:true block with algorithm, salt, and keys. - Keep the field omitted unless a user opts into local installed-state badges. --- README.md | 5 +++++ dist/client.d.ts | 4 ++-- dist/server.d.ts | 4 ++-- dist/types.d.ts | 8 ++++++++ src/__tests__/server.test.ts | 6 ++++++ src/client.ts | 4 ++++ src/server.ts | 4 ++++ src/types.ts | 10 ++++++++++ 8 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d694ab7..2f19a1e 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,15 @@ const decrypted = callback.watcher(); - `ServerData` - Server information structure - `UserInfo` - User information structure - `CommunityAppsLaunch` - Unraid OS to Community Apps iframe launch action +- `CommunityAppsInstalledApps` - Explicit opt-in installed app fingerprints for local CA installed-state UI - `ExternalActions` - Union type of all external actions - `UpcActions` - Union type of all UPC actions - `QueryPayloads` - Union type of all payload types +`CommunityAppsLaunch.installedApps` should be omitted by default. Include it +only after the user opts into local installed-state UI, with `enabled: true`, +`algorithm: "sha256"`, a launch-specific `salt`, and salted fingerprint `keys`. + ### Store Interface ```typescript diff --git a/dist/client.d.ts b/dist/client.d.ts index ea4f7cd..5b2f554 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; export declare const createCallback: (config: CallbackConfig) => { send: (url: string, payload: SendPayloads, redirectType?: "newTab" | "replace" | null, sendType?: string, sender?: string) => void; parse: (data: string, options?: { @@ -19,4 +19,4 @@ export declare const useCallback: (config: CallbackConfig) => { watcher: (options?: WatcherOptions) => QueryPayloads | undefined; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/server.d.ts b/dist/server.d.ts index 7708498..c387370 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; /** * Server-safe factory that exposes only parse and generateUrl. * @@ -11,4 +11,4 @@ export declare const createServerCallback: (config: CallbackConfig) => { }) => QueryPayloads; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/types.d.ts b/dist/types.d.ts index a6c5fb5..d67b141 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -95,9 +95,17 @@ export interface ServerTroubleshoot { type: Troubleshoot; server: ServerData; } +export type CommunityAppsInstalledAppsAlgorithm = "sha256"; +export interface CommunityAppsInstalledApps { + enabled: true; + algorithm: CommunityAppsInstalledAppsAlgorithm; + salt: string; + keys: string[]; +} export interface CommunityAppsLaunch { type: CommunityApps; server: ServerData; + installedApps?: CommunityAppsInstalledApps; installUrl?: string; installUrlTemplate?: string; installParam?: string; diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 5bf17de..ab86bdb 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -85,6 +85,12 @@ describe('createServerCallback (server entry)', () => { }, installUrlTemplate: '/Apps/AddContainer?xmlTemplate={templateUrl}', installTarget: '_top', + installedApps: { + enabled: true, + algorithm: 'sha256', + salt: 'launch-salt', + keys: ['sha256:installed-app-fingerprint'], + }, path: '/apps', theme: 'dark', }, diff --git a/src/client.ts b/src/client.ts index f8a61ca..f3b3d55 100644 --- a/src/client.ts +++ b/src/client.ts @@ -36,6 +36,8 @@ import type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -223,6 +225,8 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/server.ts b/src/server.ts index 619a805..33a81c1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -35,6 +35,8 @@ import type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -123,6 +125,8 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/types.ts b/src/types.ts index dbea798..14f08e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -162,9 +162,19 @@ export interface ServerTroubleshoot { server: ServerData; } +export type CommunityAppsInstalledAppsAlgorithm = "sha256"; + +export interface CommunityAppsInstalledApps { + enabled: true; + algorithm: CommunityAppsInstalledAppsAlgorithm; + salt: string; + keys: string[]; +} + export interface CommunityAppsLaunch { type: CommunityApps; server: ServerData; + installedApps?: CommunityAppsInstalledApps; installUrl?: string; installUrlTemplate?: string; installParam?: string; From 862d3e068b0cf1bf9e92cf1f43387d03bba37820 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 2 Jun 2026 14:06:32 -0400 Subject: [PATCH 3/5] feat(callbacks): compact installed app status payloads - Replace the flat installed app fingerprint list with a compact status map. - Before, callbacks could only say that a hash was present, so CA could not distinguish currently installed apps from previously installed apps. - Variable-length hash strings also risked wasting URL space in hash-based iframe launches. - Add numeric installed app status codes and fixed sha256-128 app fingerprints encoded as 22-character base64url keys. - Export the status enum through client and server entrypoints so Core can build payloads without hardcoding status values. --- README.md | 7 +++++-- dist/client.d.ts | 6 ++++-- dist/client.js | 9 ++++++++- dist/index.js | 9 ++++++++- dist/server.d.ts | 6 ++++-- dist/server.js | 9 ++++++++- dist/types.d.ts | 10 ++++++++-- src/__tests__/server.test.ts | 9 ++++++--- src/client.ts | 7 +++++++ src/server.ts | 7 +++++++ src/types.ts | 16 ++++++++++++++-- 11 files changed, 79 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2f19a1e..61d2e80 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,17 @@ const decrypted = callback.watcher(); - `ServerData` - Server information structure - `UserInfo` - User information structure - `CommunityAppsLaunch` - Unraid OS to Community Apps iframe launch action -- `CommunityAppsInstalledApps` - Explicit opt-in installed app fingerprints for local CA installed-state UI +- `CommunityAppsInstalledApps` - Explicit opt-in installed app status map for local CA installed-state UI +- `CommunityAppsInstalledAppStatus` - Compact installed app status enum (`Installed = 1`, `PreviouslyInstalled = 2`) - `ExternalActions` - Union type of all external actions - `UpcActions` - Union type of all UPC actions - `QueryPayloads` - Union type of all payload types `CommunityAppsLaunch.installedApps` should be omitted by default. Include it only after the user opts into local installed-state UI, with `enabled: true`, -`algorithm: "sha256"`, a launch-specific `salt`, and salted fingerprint `keys`. +`algorithm: "sha256-128"`, a launch-specific `salt`, and an `apps` status map. +Each app key should be a salted SHA-256 fingerprint truncated to 128 bits and +encoded as unpadded base64url, producing a fixed 22-character key. ### Store Interface diff --git a/dist/client.d.ts b/dist/client.d.ts index 5b2f554..5cb51e2 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,4 +1,5 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import { CommunityAppsInstalledAppStatus } from "./types.js"; export declare const createCallback: (config: CallbackConfig) => { send: (url: string, payload: SendPayloads, redirectType?: "newTab" | "replace" | null, sendType?: string, sender?: string) => void; parse: (data: string, options?: { @@ -19,4 +20,5 @@ export declare const useCallback: (config: CallbackConfig) => { watcher: (options?: WatcherOptions) => QueryPayloads | undefined; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export { CommunityAppsInstalledAppStatus }; +export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/client.js b/dist/client.js index a84a995..566c491 100644 --- a/dist/client.js +++ b/dist/client.js @@ -2029,6 +2029,13 @@ var require_enc_utf8 = __commonJS({ } }); +// src/types.ts +var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["PreviouslyInstalled"] = 2] = "PreviouslyInstalled"; + return CommunityAppsInstalledAppStatus2; +})(CommunityAppsInstalledAppStatus || {}); + // src/core.ts var import_aes = __toESM(require_aes()); var import_enc_utf8 = __toESM(require_enc_utf8()); @@ -2162,4 +2169,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { createCallback, useCallback }; +export { CommunityAppsInstalledAppStatus, createCallback, useCallback }; diff --git a/dist/index.js b/dist/index.js index a84a995..566c491 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2029,6 +2029,13 @@ var require_enc_utf8 = __commonJS({ } }); +// src/types.ts +var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["PreviouslyInstalled"] = 2] = "PreviouslyInstalled"; + return CommunityAppsInstalledAppStatus2; +})(CommunityAppsInstalledAppStatus || {}); + // src/core.ts var import_aes = __toESM(require_aes()); var import_enc_utf8 = __toESM(require_enc_utf8()); @@ -2162,4 +2169,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { createCallback, useCallback }; +export { CommunityAppsInstalledAppStatus, createCallback, useCallback }; diff --git a/dist/server.d.ts b/dist/server.d.ts index c387370..6a0014d 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,4 +1,5 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import { CommunityAppsInstalledAppStatus } from "./types.js"; /** * Server-safe factory that exposes only parse and generateUrl. * @@ -11,4 +12,5 @@ export declare const createServerCallback: (config: CallbackConfig) => { }) => QueryPayloads; generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; -export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export { CommunityAppsInstalledAppStatus }; +export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/server.js b/dist/server.js index 1c39717..0fed84b 100644 --- a/dist/server.js +++ b/dist/server.js @@ -2029,6 +2029,13 @@ var require_enc_utf8 = __commonJS({ } }); +// src/types.ts +var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; + CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["PreviouslyInstalled"] = 2] = "PreviouslyInstalled"; + return CommunityAppsInstalledAppStatus2; +})(CommunityAppsInstalledAppStatus || {}); + // src/core.ts var import_aes = __toESM(require_aes()); var import_enc_utf8 = __toESM(require_enc_utf8()); @@ -2101,4 +2108,4 @@ var createServerCallback = (config) => { }; }; -export { createServerCallback }; +export { CommunityAppsInstalledAppStatus, createServerCallback }; diff --git a/dist/types.d.ts b/dist/types.d.ts index d67b141..99bdc16 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -95,12 +95,18 @@ export interface ServerTroubleshoot { type: Troubleshoot; server: ServerData; } -export type CommunityAppsInstalledAppsAlgorithm = "sha256"; +export type CommunityAppsInstalledAppsAlgorithm = "sha256-128"; +export declare enum CommunityAppsInstalledAppStatus { + Installed = 1, + PreviouslyInstalled = 2 +} +export type CommunityAppsInstalledAppHash = string; +export type CommunityAppsInstalledAppStatusMap = Record; export interface CommunityAppsInstalledApps { enabled: true; algorithm: CommunityAppsInstalledAppsAlgorithm; salt: string; - keys: string[]; + apps: CommunityAppsInstalledAppStatusMap; } export interface CommunityAppsLaunch { type: CommunityApps; diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index ab86bdb..59047ef 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { createServerCallback } from '../server' +import { CommunityAppsInstalledAppStatus, createServerCallback } from '../server' import type { CommunityAppsLaunch, ServerPayload } from '../types' describe('createServerCallback (server entry)', () => { @@ -87,9 +87,12 @@ describe('createServerCallback (server entry)', () => { installTarget: '_top', installedApps: { enabled: true, - algorithm: 'sha256', + algorithm: 'sha256-128', salt: 'launch-salt', - keys: ['sha256:installed-app-fingerprint'], + apps: { + 'a23456789012345678901A': CommunityAppsInstalledAppStatus.Installed, + 'b23456789012345678901B': CommunityAppsInstalledAppStatus.PreviouslyInstalled, + }, }, path: '/apps', theme: 'dark', diff --git a/src/client.ts b/src/client.ts index f3b3d55..172e82a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -37,6 +37,8 @@ import type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, @@ -44,6 +46,7 @@ import type { ExternalPayload, UpcPayload, } from "./types.js"; +import { CommunityAppsInstalledAppStatus } from "./types.js"; import { appendEncryptedDataToUrl, createEncryptedPayload, @@ -186,6 +189,8 @@ export const createCallback = (config: CallbackConfig) => { */ export const useCallback = createCallback; +export { CommunityAppsInstalledAppStatus }; + // Re-export all types for convenience from the client entry. export type { CallbackConfig, @@ -226,6 +231,8 @@ export type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, diff --git a/src/server.ts b/src/server.ts index 33a81c1..c57388b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,6 +36,8 @@ import type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, @@ -43,6 +45,7 @@ import type { ExternalPayload, UpcPayload, } from "./types.js"; +import { CommunityAppsInstalledAppStatus } from "./types.js"; import { appendEncryptedDataToUrl, createEncryptedPayload, @@ -87,6 +90,8 @@ export const createServerCallback = (config: CallbackConfig) => { }; }; +export { CommunityAppsInstalledAppStatus }; + // Re-export all types for convenience from the server entry. export type { CallbackConfig, @@ -126,6 +131,8 @@ export type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, diff --git a/src/types.ts b/src/types.ts index 14f08e1..2680f38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -162,13 +162,25 @@ export interface ServerTroubleshoot { server: ServerData; } -export type CommunityAppsInstalledAppsAlgorithm = "sha256"; +export type CommunityAppsInstalledAppsAlgorithm = "sha256-128"; + +export enum CommunityAppsInstalledAppStatus { + Installed = 1, + PreviouslyInstalled = 2, +} + +export type CommunityAppsInstalledAppHash = string; + +export type CommunityAppsInstalledAppStatusMap = Record< + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatus +>; export interface CommunityAppsInstalledApps { enabled: true; algorithm: CommunityAppsInstalledAppsAlgorithm; salt: string; - keys: string[]; + apps: CommunityAppsInstalledAppStatusMap; } export interface CommunityAppsLaunch { From f40f495438f7e3b16b3f2118760ff84c2d63a592 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 3 Jun 2026 00:16:51 -0400 Subject: [PATCH 4/5] feat(callbacks): add Community Apps status lookup bridge - Add lookup mode for Community Apps installed app status payloads. - Before, status context had to be carried as a launch-time app map when CA needed installed-state UI. - That exposed more installed-app information than necessary and could grow callback URLs. - Add fixed-length salted app hash helpers and a post-me parent bridge for visible-app status lookups. - Keep batch maps available while making lookup mode the preferred privacy-preserving integration. --- README.md | 13 ++- dist/client.d.ts | 6 +- dist/client.js | 166 +++++++++++++++++++++++++++++++- dist/community-apps-client.d.ts | 16 +++ dist/community-apps.d.ts | 5 + dist/index.js | 166 +++++++++++++++++++++++++++++++- dist/server.d.ts | 5 +- dist/server.js | 142 ++++++++++++++++++++++++++- dist/types.d.ts | 19 +++- package.json | 3 +- pnpm-lock.yaml | 8 ++ src/__tests__/server.test.ts | 20 +++- src/client.ts | 22 +++++ src/community-apps-client.ts | 53 ++++++++++ src/community-apps.ts | 32 ++++++ src/post-me.d.ts | 16 +++ src/server.ts | 16 +++ src/types.ts | 28 +++++- 18 files changed, 708 insertions(+), 28 deletions(-) create mode 100644 dist/community-apps-client.d.ts create mode 100644 dist/community-apps.d.ts create mode 100644 src/community-apps-client.ts create mode 100644 src/community-apps.ts create mode 100644 src/post-me.d.ts diff --git a/README.md b/README.md index 61d2e80..e1d6b7b 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,22 @@ const decrypted = callback.watcher(); - `ServerData` - Server information structure - `UserInfo` - User information structure - `CommunityAppsLaunch` - Unraid OS to Community Apps iframe launch action -- `CommunityAppsInstalledApps` - Explicit opt-in installed app status map for local CA installed-state UI +- `CommunityAppsInstalledApps` - Explicit opt-in installed app status lookup or status map for local CA installed-state UI - `CommunityAppsInstalledAppStatus` - Compact installed app status enum (`Installed = 1`, `PreviouslyInstalled = 2`) +- `createCommunityAppsInstalledAppsHostBridge` - `post-me` parent-side helper for iframe status lookups +- `createCommunityAppsInstalledAppHash` - Fixed-length salted app fingerprint helper - `ExternalActions` - Union type of all external actions - `UpcActions` - Union type of all UPC actions - `QueryPayloads` - Union type of all payload types `CommunityAppsLaunch.installedApps` should be omitted by default. Include it only after the user opts into local installed-state UI, with `enabled: true`, -`algorithm: "sha256-128"`, a launch-specific `salt`, and an `apps` status map. -Each app key should be a salted SHA-256 fingerprint truncated to 128 bits and -encoded as unpadded base64url, producing a fixed 22-character key. +`algorithm: "sha256-128"`, a launch-specific `salt`, and preferably +`mode: "lookup"`. Lookup mode lets the Community Apps iframe request statuses +only for visible app hashes over the `post-me` bridge, so the full installed +inventory is not sent to CA. Batch mode remains available with an `apps` status +map. Each app key should be a salted SHA-256 fingerprint truncated to 128 bits +and encoded as unpadded base64url, producing a fixed 22-character key. ### Store Interface diff --git a/dist/client.d.ts b/dist/client.d.ts index 5cb51e2..1a209d9 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,5 +1,7 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; import { CommunityAppsInstalledAppStatus } from "./types.js"; +export { COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, createCommunityAppsInstalledAppHash, isCommunityAppsInstalledAppHash, } from "./community-apps.js"; +export { createCommunityAppsInstalledAppsHostBridge, type CommunityAppsInstalledAppsHostBridge, type CommunityAppsInstalledAppsHostMethods, type CreateCommunityAppsInstalledAppsHostBridgeOptions, } from "./community-apps-client.js"; export declare const createCallback: (config: CallbackConfig) => { send: (url: string, payload: SendPayloads, redirectType?: "newTab" | "replace" | null, sendType?: string, sender?: string) => void; parse: (data: string, options?: { @@ -21,4 +23,4 @@ export declare const useCallback: (config: CallbackConfig) => { generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; export { CommunityAppsInstalledAppStatus }; -export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/client.js b/dist/client.js index 566c491..912db05 100644 --- a/dist/client.js +++ b/dist/client.js @@ -1,3 +1,5 @@ +import { ParentHandshake, WindowMessenger } from 'post-me'; + var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; @@ -1234,7 +1236,7 @@ var require_cipher_core = __commonJS({ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; var C_enc = C.enc; C_enc.Utf8; - var Base64 = C_enc.Base64; + var Base642 = C_enc.Base64; var C_algo = C.algo; var EvpKDF = C_algo.EvpKDF; var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ @@ -1644,7 +1646,7 @@ var require_cipher_core = __commonJS({ } else { wordArray = ciphertext; } - return wordArray.toString(Base64); + return wordArray.toString(Base642); }, /** * Converts an OpenSSL-compatible string to a cipher params object. @@ -1661,7 +1663,7 @@ var require_cipher_core = __commonJS({ */ parse: function(openSSLStr) { var salt; - var ciphertext = Base64.parse(openSSLStr); + var ciphertext = Base642.parse(openSSLStr); var ciphertextWords = ciphertext.words; if (ciphertextWords[0] == 1398893684 && ciphertextWords[1] == 1701076831) { salt = WordArray.create(ciphertextWords.slice(2, 4)); @@ -2029,6 +2031,127 @@ var require_enc_utf8 = __commonJS({ } }); +// node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js +var require_sha256 = __commonJS({ + "node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js"(exports$1, module) { + (function(root, factory) { + if (typeof exports$1 === "object") { + module.exports = exports$1 = factory(require_core()); + } else if (typeof define === "function" && define.amd) { + define(["./core"], factory); + } else { + factory(root.CryptoJS); + } + })(exports$1, function(CryptoJS) { + (function(Math2) { + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + var H = []; + var K = []; + (function() { + function isPrime(n2) { + var sqrtN = Math2.sqrt(n2); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n2 % factor)) { + return false; + } + } + return true; + } + function getFractionalBits(n2) { + return (n2 - (n2 | 0)) * 4294967296 | 0; + } + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math2.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math2.pow(n, 1 / 3)); + nPrime++; + } + n++; + } + })(); + var W = []; + var SHA2562 = C_algo.SHA256 = Hasher.extend({ + _doReset: function() { + this._hash = new WordArray.init(H.slice(0)); + }, + _doProcessBlock: function(M, offset) { + var H2 = this._hash.words; + var a = H2[0]; + var b = H2[1]; + var c = H2[2]; + var d = H2[3]; + var e = H2[4]; + var f = H2[5]; + var g = H2[6]; + var h = H2[7]; + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3; + var gamma1x = W[i - 2]; + var gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10; + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + var ch = e & f ^ ~e & g; + var maj = a & b ^ a & c ^ b & c; + var sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22); + var sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25); + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + h = g; + g = f; + f = e; + e = d + t1 | 0; + d = c; + c = b; + b = a; + a = t1 + t2 | 0; + } + H2[0] = H2[0] + a | 0; + H2[1] = H2[1] + b | 0; + H2[2] = H2[2] + c | 0; + H2[3] = H2[3] + d | 0; + H2[4] = H2[4] + e | 0; + H2[5] = H2[5] + f | 0; + H2[6] = H2[6] + g | 0; + H2[7] = H2[7] + h | 0; + }, + _doFinalize: function() { + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + dataWords[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32; + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math2.floor(nBitsTotal / 4294967296); + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + this._process(); + return this._hash; + }, + clone: function() { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + return clone; + } + }); + C.SHA256 = Hasher._createHelper(SHA2562); + C.HmacSHA256 = Hasher._createHmacHelper(SHA2562); + })(Math); + return CryptoJS.SHA256; + }); + } +}); + // src/types.ts var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; @@ -2086,6 +2209,41 @@ var appendEncryptedDataToUrl = (url, encryptedData, useHash) => { return destinationUrl.toString(); }; +// src/community-apps.ts +var import_sha256 = __toESM(require_sha256()); +var import_enc_base64 = __toESM(require_enc_base64()); +var COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM = "sha256-128"; +var COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH = 22; +var createCommunityAppsInstalledAppHash = (stableIdentifier, salt) => { + const digest = (0, import_sha256.default)(`${salt}\0${stableIdentifier}`); + digest.sigBytes = 16; + digest.clamp(); + return digest.toString(import_enc_base64.default).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +}; +var isCommunityAppsInstalledAppHash = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{22}$/.test(value) && value.length === COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH; +var createCommunityAppsInstalledAppsHostBridge = async ({ + iframeWindow, + iframeOrigin, + methods, + localWindow, + maxHandshakeAttempts = 20, + handshakeAttemptIntervalMs = 100 +}) => { + const connection = await ParentHandshake( + new WindowMessenger({ + localWindow, + remoteWindow: iframeWindow, + remoteOrigin: iframeOrigin + }), + methods, + maxHandshakeAttempts, + handshakeAttemptIntervalMs + ); + return { + close: () => connection.close() + }; +}; + // src/client.ts var createCallback = (config) => { const shouldUseHash = config.useHash !== false; @@ -2169,4 +2327,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { CommunityAppsInstalledAppStatus, createCallback, useCallback }; +export { COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, CommunityAppsInstalledAppStatus, createCallback, createCommunityAppsInstalledAppHash, createCommunityAppsInstalledAppsHostBridge, isCommunityAppsInstalledAppHash, useCallback }; diff --git a/dist/community-apps-client.d.ts b/dist/community-apps-client.d.ts new file mode 100644 index 0000000..70fe67e --- /dev/null +++ b/dist/community-apps-client.d.ts @@ -0,0 +1,16 @@ +import type { CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse } from "./types.js"; +export type CommunityAppsInstalledAppsHostMethods = { + getInstalledAppStatuses: (request: CommunityAppsInstalledAppStatusRequest) => CommunityAppsInstalledAppStatusResponse | Promise; +}; +export type CommunityAppsInstalledAppsHostBridge = { + close: () => void; +}; +export type CreateCommunityAppsInstalledAppsHostBridgeOptions = { + iframeWindow: Window; + iframeOrigin: string; + methods: CommunityAppsInstalledAppsHostMethods; + localWindow?: Window; + maxHandshakeAttempts?: number; + handshakeAttemptIntervalMs?: number; +}; +export declare const createCommunityAppsInstalledAppsHostBridge: ({ iframeWindow, iframeOrigin, methods, localWindow, maxHandshakeAttempts, handshakeAttemptIntervalMs, }: CreateCommunityAppsInstalledAppsHostBridgeOptions) => Promise; diff --git a/dist/community-apps.d.ts b/dist/community-apps.d.ts new file mode 100644 index 0000000..0a72281 --- /dev/null +++ b/dist/community-apps.d.ts @@ -0,0 +1,5 @@ +import type { CommunityAppsInstalledAppHash, CommunityAppsInstalledAppsAlgorithm } from "./types.js"; +export declare const COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM: CommunityAppsInstalledAppsAlgorithm; +export declare const COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH = 22; +export declare const createCommunityAppsInstalledAppHash: (stableIdentifier: string, salt: string) => CommunityAppsInstalledAppHash; +export declare const isCommunityAppsInstalledAppHash: (value: unknown) => value is CommunityAppsInstalledAppHash; diff --git a/dist/index.js b/dist/index.js index 566c491..912db05 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,3 +1,5 @@ +import { ParentHandshake, WindowMessenger } from 'post-me'; + var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; @@ -1234,7 +1236,7 @@ var require_cipher_core = __commonJS({ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; var C_enc = C.enc; C_enc.Utf8; - var Base64 = C_enc.Base64; + var Base642 = C_enc.Base64; var C_algo = C.algo; var EvpKDF = C_algo.EvpKDF; var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ @@ -1644,7 +1646,7 @@ var require_cipher_core = __commonJS({ } else { wordArray = ciphertext; } - return wordArray.toString(Base64); + return wordArray.toString(Base642); }, /** * Converts an OpenSSL-compatible string to a cipher params object. @@ -1661,7 +1663,7 @@ var require_cipher_core = __commonJS({ */ parse: function(openSSLStr) { var salt; - var ciphertext = Base64.parse(openSSLStr); + var ciphertext = Base642.parse(openSSLStr); var ciphertextWords = ciphertext.words; if (ciphertextWords[0] == 1398893684 && ciphertextWords[1] == 1701076831) { salt = WordArray.create(ciphertextWords.slice(2, 4)); @@ -2029,6 +2031,127 @@ var require_enc_utf8 = __commonJS({ } }); +// node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js +var require_sha256 = __commonJS({ + "node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js"(exports$1, module) { + (function(root, factory) { + if (typeof exports$1 === "object") { + module.exports = exports$1 = factory(require_core()); + } else if (typeof define === "function" && define.amd) { + define(["./core"], factory); + } else { + factory(root.CryptoJS); + } + })(exports$1, function(CryptoJS) { + (function(Math2) { + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + var H = []; + var K = []; + (function() { + function isPrime(n2) { + var sqrtN = Math2.sqrt(n2); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n2 % factor)) { + return false; + } + } + return true; + } + function getFractionalBits(n2) { + return (n2 - (n2 | 0)) * 4294967296 | 0; + } + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math2.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math2.pow(n, 1 / 3)); + nPrime++; + } + n++; + } + })(); + var W = []; + var SHA2562 = C_algo.SHA256 = Hasher.extend({ + _doReset: function() { + this._hash = new WordArray.init(H.slice(0)); + }, + _doProcessBlock: function(M, offset) { + var H2 = this._hash.words; + var a = H2[0]; + var b = H2[1]; + var c = H2[2]; + var d = H2[3]; + var e = H2[4]; + var f = H2[5]; + var g = H2[6]; + var h = H2[7]; + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3; + var gamma1x = W[i - 2]; + var gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10; + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + var ch = e & f ^ ~e & g; + var maj = a & b ^ a & c ^ b & c; + var sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22); + var sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25); + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + h = g; + g = f; + f = e; + e = d + t1 | 0; + d = c; + c = b; + b = a; + a = t1 + t2 | 0; + } + H2[0] = H2[0] + a | 0; + H2[1] = H2[1] + b | 0; + H2[2] = H2[2] + c | 0; + H2[3] = H2[3] + d | 0; + H2[4] = H2[4] + e | 0; + H2[5] = H2[5] + f | 0; + H2[6] = H2[6] + g | 0; + H2[7] = H2[7] + h | 0; + }, + _doFinalize: function() { + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + dataWords[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32; + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math2.floor(nBitsTotal / 4294967296); + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + this._process(); + return this._hash; + }, + clone: function() { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + return clone; + } + }); + C.SHA256 = Hasher._createHelper(SHA2562); + C.HmacSHA256 = Hasher._createHmacHelper(SHA2562); + })(Math); + return CryptoJS.SHA256; + }); + } +}); + // src/types.ts var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; @@ -2086,6 +2209,41 @@ var appendEncryptedDataToUrl = (url, encryptedData, useHash) => { return destinationUrl.toString(); }; +// src/community-apps.ts +var import_sha256 = __toESM(require_sha256()); +var import_enc_base64 = __toESM(require_enc_base64()); +var COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM = "sha256-128"; +var COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH = 22; +var createCommunityAppsInstalledAppHash = (stableIdentifier, salt) => { + const digest = (0, import_sha256.default)(`${salt}\0${stableIdentifier}`); + digest.sigBytes = 16; + digest.clamp(); + return digest.toString(import_enc_base64.default).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +}; +var isCommunityAppsInstalledAppHash = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{22}$/.test(value) && value.length === COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH; +var createCommunityAppsInstalledAppsHostBridge = async ({ + iframeWindow, + iframeOrigin, + methods, + localWindow, + maxHandshakeAttempts = 20, + handshakeAttemptIntervalMs = 100 +}) => { + const connection = await ParentHandshake( + new WindowMessenger({ + localWindow, + remoteWindow: iframeWindow, + remoteOrigin: iframeOrigin + }), + methods, + maxHandshakeAttempts, + handshakeAttemptIntervalMs + ); + return { + close: () => connection.close() + }; +}; + // src/client.ts var createCallback = (config) => { const shouldUseHash = config.useHash !== false; @@ -2169,4 +2327,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { CommunityAppsInstalledAppStatus, createCallback, useCallback }; +export { COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, CommunityAppsInstalledAppStatus, createCallback, createCommunityAppsInstalledAppHash, createCommunityAppsInstalledAppsHostBridge, isCommunityAppsInstalledAppHash, useCallback }; diff --git a/dist/server.d.ts b/dist/server.d.ts index 6a0014d..565b8d8 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,5 +1,6 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; import { CommunityAppsInstalledAppStatus } from "./types.js"; +export { COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, createCommunityAppsInstalledAppHash, isCommunityAppsInstalledAppHash, } from "./community-apps.js"; /** * Server-safe factory that exposes only parse and generateUrl. * @@ -13,4 +14,4 @@ export declare const createServerCallback: (config: CallbackConfig) => { generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; export { CommunityAppsInstalledAppStatus }; -export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledApps, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/server.js b/dist/server.js index 0fed84b..5beb607 100644 --- a/dist/server.js +++ b/dist/server.js @@ -1234,7 +1234,7 @@ var require_cipher_core = __commonJS({ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm; var C_enc = C.enc; C_enc.Utf8; - var Base64 = C_enc.Base64; + var Base642 = C_enc.Base64; var C_algo = C.algo; var EvpKDF = C_algo.EvpKDF; var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({ @@ -1644,7 +1644,7 @@ var require_cipher_core = __commonJS({ } else { wordArray = ciphertext; } - return wordArray.toString(Base64); + return wordArray.toString(Base642); }, /** * Converts an OpenSSL-compatible string to a cipher params object. @@ -1661,7 +1661,7 @@ var require_cipher_core = __commonJS({ */ parse: function(openSSLStr) { var salt; - var ciphertext = Base64.parse(openSSLStr); + var ciphertext = Base642.parse(openSSLStr); var ciphertextWords = ciphertext.words; if (ciphertextWords[0] == 1398893684 && ciphertextWords[1] == 1701076831) { salt = WordArray.create(ciphertextWords.slice(2, 4)); @@ -2029,6 +2029,127 @@ var require_enc_utf8 = __commonJS({ } }); +// node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js +var require_sha256 = __commonJS({ + "node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/sha256.js"(exports$1, module) { + (function(root, factory) { + if (typeof exports$1 === "object") { + module.exports = exports$1 = factory(require_core()); + } else if (typeof define === "function" && define.amd) { + define(["./core"], factory); + } else { + factory(root.CryptoJS); + } + })(exports$1, function(CryptoJS) { + (function(Math2) { + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + var H = []; + var K = []; + (function() { + function isPrime(n2) { + var sqrtN = Math2.sqrt(n2); + for (var factor = 2; factor <= sqrtN; factor++) { + if (!(n2 % factor)) { + return false; + } + } + return true; + } + function getFractionalBits(n2) { + return (n2 - (n2 | 0)) * 4294967296 | 0; + } + var n = 2; + var nPrime = 0; + while (nPrime < 64) { + if (isPrime(n)) { + if (nPrime < 8) { + H[nPrime] = getFractionalBits(Math2.pow(n, 1 / 2)); + } + K[nPrime] = getFractionalBits(Math2.pow(n, 1 / 3)); + nPrime++; + } + n++; + } + })(); + var W = []; + var SHA2562 = C_algo.SHA256 = Hasher.extend({ + _doReset: function() { + this._hash = new WordArray.init(H.slice(0)); + }, + _doProcessBlock: function(M, offset) { + var H2 = this._hash.words; + var a = H2[0]; + var b = H2[1]; + var c = H2[2]; + var d = H2[3]; + var e = H2[4]; + var f = H2[5]; + var g = H2[6]; + var h = H2[7]; + for (var i = 0; i < 64; i++) { + if (i < 16) { + W[i] = M[offset + i] | 0; + } else { + var gamma0x = W[i - 15]; + var gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3; + var gamma1x = W[i - 2]; + var gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10; + W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; + } + var ch = e & f ^ ~e & g; + var maj = a & b ^ a & c ^ b & c; + var sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22); + var sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25); + var t1 = h + sigma1 + ch + K[i] + W[i]; + var t2 = sigma0 + maj; + h = g; + g = f; + f = e; + e = d + t1 | 0; + d = c; + c = b; + b = a; + a = t1 + t2 | 0; + } + H2[0] = H2[0] + a | 0; + H2[1] = H2[1] + b | 0; + H2[2] = H2[2] + c | 0; + H2[3] = H2[3] + d | 0; + H2[4] = H2[4] + e | 0; + H2[5] = H2[5] + f | 0; + H2[6] = H2[6] + g | 0; + H2[7] = H2[7] + h | 0; + }, + _doFinalize: function() { + var data = this._data; + var dataWords = data.words; + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + dataWords[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32; + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math2.floor(nBitsTotal / 4294967296); + dataWords[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal; + data.sigBytes = dataWords.length * 4; + this._process(); + return this._hash; + }, + clone: function() { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + return clone; + } + }); + C.SHA256 = Hasher._createHelper(SHA2562); + C.HmacSHA256 = Hasher._createHmacHelper(SHA2562); + })(Math); + return CryptoJS.SHA256; + }); + } +}); + // src/types.ts var CommunityAppsInstalledAppStatus = /* @__PURE__ */ ((CommunityAppsInstalledAppStatus2) => { CommunityAppsInstalledAppStatus2[CommunityAppsInstalledAppStatus2["Installed"] = 1] = "Installed"; @@ -2086,6 +2207,19 @@ var appendEncryptedDataToUrl = (url, encryptedData, useHash) => { return destinationUrl.toString(); }; +// src/community-apps.ts +var import_sha256 = __toESM(require_sha256()); +var import_enc_base64 = __toESM(require_enc_base64()); +var COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM = "sha256-128"; +var COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH = 22; +var createCommunityAppsInstalledAppHash = (stableIdentifier, salt) => { + const digest = (0, import_sha256.default)(`${salt}\0${stableIdentifier}`); + digest.sigBytes = 16; + digest.clamp(); + return digest.toString(import_enc_base64.default).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +}; +var isCommunityAppsInstalledAppHash = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{22}$/.test(value) && value.length === COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH; + // src/server.ts var createServerCallback = (config) => { const parse = (data, options) => { @@ -2108,4 +2242,4 @@ var createServerCallback = (config) => { }; }; -export { CommunityAppsInstalledAppStatus, createServerCallback }; +export { COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, CommunityAppsInstalledAppStatus, createCommunityAppsInstalledAppHash, createServerCallback, isCommunityAppsInstalledAppHash }; diff --git a/dist/types.d.ts b/dist/types.d.ts index 99bdc16..0680bc0 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -96,16 +96,33 @@ export interface ServerTroubleshoot { server: ServerData; } export type CommunityAppsInstalledAppsAlgorithm = "sha256-128"; +export type CommunityAppsInstalledAppsMode = "batch" | "lookup"; export declare enum CommunityAppsInstalledAppStatus { Installed = 1, PreviouslyInstalled = 2 } export type CommunityAppsInstalledAppHash = string; export type CommunityAppsInstalledAppStatusMap = Record; -export interface CommunityAppsInstalledApps { +export interface CommunityAppsInstalledAppsBase { enabled: true; algorithm: CommunityAppsInstalledAppsAlgorithm; + mode?: CommunityAppsInstalledAppsMode; salt: string; +} +export interface CommunityAppsInstalledAppsBatch extends CommunityAppsInstalledAppsBase { + mode?: "batch"; + apps: CommunityAppsInstalledAppStatusMap; +} +export interface CommunityAppsInstalledAppsLookup extends CommunityAppsInstalledAppsBase { + mode: "lookup"; +} +export type CommunityAppsInstalledApps = CommunityAppsInstalledAppsBatch | CommunityAppsInstalledAppsLookup; +export interface CommunityAppsInstalledAppStatusRequest { + algorithm: CommunityAppsInstalledAppsAlgorithm; + salt: string; + appHashes: CommunityAppsInstalledAppHash[]; +} +export interface CommunityAppsInstalledAppStatusResponse { apps: CommunityAppsInstalledAppStatusMap; } export interface CommunityAppsLaunch { diff --git a/package.json b/package.json index 9a922b7..88870b0 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "test:ui": "vitest --ui" }, "dependencies": { - "crypto-js": "^4.2.0" + "crypto-js": "^4.2.0", + "post-me": "^0.4.5" }, "devDependencies": { "@types/crypto-js": "^4.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2fa959..70d7834 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + post-me: + specifier: ^0.4.5 + version: 0.4.5 devDependencies: '@types/crypto-js': specifier: ^4.2.1 @@ -1203,6 +1206,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + post-me@0.4.5: + resolution: {integrity: sha512-XgPdktF/2M5jglgVDULr9NUb/QNv3bY3g6RG22iTb5MIMtB07/5FJB5fbVmu5Eaopowc6uZx7K3e7x1shPwnXw==} + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2516,6 +2522,8 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 + post-me@0.4.5: {} + postcss-load-config@6.0.1(postcss@8.5.8): dependencies: lilconfig: 3.1.3 diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 59047ef..4731bf4 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect } from 'vitest' -import { CommunityAppsInstalledAppStatus, createServerCallback } from '../server' +import { + createCommunityAppsInstalledAppHash, + createServerCallback, +} from '../server' import type { CommunityAppsLaunch, ServerPayload } from '../types' describe('createServerCallback (server entry)', () => { @@ -88,11 +91,8 @@ describe('createServerCallback (server entry)', () => { installedApps: { enabled: true, algorithm: 'sha256-128', + mode: 'lookup', salt: 'launch-salt', - apps: { - 'a23456789012345678901A': CommunityAppsInstalledAppStatus.Installed, - 'b23456789012345678901B': CommunityAppsInstalledAppStatus.PreviouslyInstalled, - }, }, path: '/apps', theme: 'dark', @@ -111,4 +111,14 @@ describe('createServerCallback (server entry)', () => { type: 'fromUpc', }) }) + + it('should create fixed-length Community Apps installed app hashes', () => { + const first = createCommunityAppsInstalledAppHash('https://example.com/template.xml', 'launch-salt') + const second = createCommunityAppsInstalledAppHash('https://example.com/template.xml', 'other-salt') + + expect(first).toHaveLength(22) + expect(first).toMatch(/^[A-Za-z0-9_-]{22}$/) + expect(second).toHaveLength(22) + expect(second).not.toBe(first) + }) }) diff --git a/src/client.ts b/src/client.ts index 172e82a..c06f8b6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -37,9 +37,14 @@ import type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -52,6 +57,18 @@ import { createEncryptedPayload, parseEncryptedPayload, } from "./core"; +export { + COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, + COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, + createCommunityAppsInstalledAppHash, + isCommunityAppsInstalledAppHash, +} from "./community-apps.js"; +export { + createCommunityAppsInstalledAppsHostBridge, + type CommunityAppsInstalledAppsHostBridge, + type CommunityAppsInstalledAppsHostMethods, + type CreateCommunityAppsInstalledAppsHostBridgeOptions, +} from "./community-apps-client.js"; export const createCallback = (config: CallbackConfig) => { const shouldUseHash = config.useHash !== false; @@ -231,9 +248,14 @@ export type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/community-apps-client.ts b/src/community-apps-client.ts new file mode 100644 index 0000000..6b323f1 --- /dev/null +++ b/src/community-apps-client.ts @@ -0,0 +1,53 @@ +import { + ParentHandshake, + WindowMessenger, +} from "post-me"; +import type { + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, +} from "./types.js"; + +export type CommunityAppsInstalledAppsHostMethods = { + getInstalledAppStatuses: ( + request: CommunityAppsInstalledAppStatusRequest + ) => + | CommunityAppsInstalledAppStatusResponse + | Promise; +}; + +export type CommunityAppsInstalledAppsHostBridge = { + close: () => void; +}; + +export type CreateCommunityAppsInstalledAppsHostBridgeOptions = { + iframeWindow: Window; + iframeOrigin: string; + methods: CommunityAppsInstalledAppsHostMethods; + localWindow?: Window; + maxHandshakeAttempts?: number; + handshakeAttemptIntervalMs?: number; +}; + +export const createCommunityAppsInstalledAppsHostBridge = async ({ + iframeWindow, + iframeOrigin, + methods, + localWindow, + maxHandshakeAttempts = 20, + handshakeAttemptIntervalMs = 100, +}: CreateCommunityAppsInstalledAppsHostBridgeOptions): Promise => { + const connection = await ParentHandshake( + new WindowMessenger({ + localWindow, + remoteWindow: iframeWindow, + remoteOrigin: iframeOrigin, + }), + methods, + maxHandshakeAttempts, + handshakeAttemptIntervalMs + ); + + return { + close: () => connection.close(), + }; +}; diff --git a/src/community-apps.ts b/src/community-apps.ts new file mode 100644 index 0000000..9457676 --- /dev/null +++ b/src/community-apps.ts @@ -0,0 +1,32 @@ +import SHA256 from "crypto-js/sha256.js"; +import Base64 from "crypto-js/enc-base64.js"; +import type { + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppsAlgorithm, +} from "./types.js"; + +export const COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM: CommunityAppsInstalledAppsAlgorithm = + "sha256-128"; +export const COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH = 22; + +export const createCommunityAppsInstalledAppHash = ( + stableIdentifier: string, + salt: string +): CommunityAppsInstalledAppHash => { + const digest = SHA256(`${salt}\0${stableIdentifier}`); + digest.sigBytes = 16; + digest.clamp(); + + return digest + .toString(Base64) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +}; + +export const isCommunityAppsInstalledAppHash = ( + value: unknown +): value is CommunityAppsInstalledAppHash => + typeof value === "string" && + /^[A-Za-z0-9_-]{22}$/.test(value) && + value.length === COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH; diff --git a/src/post-me.d.ts b/src/post-me.d.ts new file mode 100644 index 0000000..356b24c --- /dev/null +++ b/src/post-me.d.ts @@ -0,0 +1,16 @@ +declare module "post-me" { + export class WindowMessenger { + constructor(options: { + localWindow?: Window; + remoteWindow: Window; + remoteOrigin: string; + }); + } + + export function ParentHandshake( + messenger: WindowMessenger, + localMethods?: Record any>, + maxAttempts?: number, + attemptsInterval?: number + ): Promise<{ close: () => void }>; +} diff --git a/src/server.ts b/src/server.ts index c57388b..53cba11 100644 --- a/src/server.ts +++ b/src/server.ts @@ -36,9 +36,14 @@ import type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -51,6 +56,12 @@ import { createEncryptedPayload, parseEncryptedPayload, } from "./core.js"; +export { + COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, + COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, + createCommunityAppsInstalledAppHash, + isCommunityAppsInstalledAppHash, +} from "./community-apps.js"; /** * Server-safe factory that exposes only parse and generateUrl. @@ -131,9 +142,14 @@ export type { ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/types.ts b/src/types.ts index 2680f38..782d1ea 100644 --- a/src/types.ts +++ b/src/types.ts @@ -163,6 +163,7 @@ export interface ServerTroubleshoot { } export type CommunityAppsInstalledAppsAlgorithm = "sha256-128"; +export type CommunityAppsInstalledAppsMode = "batch" | "lookup"; export enum CommunityAppsInstalledAppStatus { Installed = 1, @@ -176,10 +177,35 @@ export type CommunityAppsInstalledAppStatusMap = Record< CommunityAppsInstalledAppStatus >; -export interface CommunityAppsInstalledApps { +export interface CommunityAppsInstalledAppsBase { enabled: true; algorithm: CommunityAppsInstalledAppsAlgorithm; + mode?: CommunityAppsInstalledAppsMode; salt: string; +} + +export interface CommunityAppsInstalledAppsBatch + extends CommunityAppsInstalledAppsBase { + mode?: "batch"; + apps: CommunityAppsInstalledAppStatusMap; +} + +export interface CommunityAppsInstalledAppsLookup + extends CommunityAppsInstalledAppsBase { + mode: "lookup"; +} + +export type CommunityAppsInstalledApps = + | CommunityAppsInstalledAppsBatch + | CommunityAppsInstalledAppsLookup; + +export interface CommunityAppsInstalledAppStatusRequest { + algorithm: CommunityAppsInstalledAppsAlgorithm; + salt: string; + appHashes: CommunityAppsInstalledAppHash[]; +} + +export interface CommunityAppsInstalledAppStatusResponse { apps: CommunityAppsInstalledAppStatusMap; } From 2f9074830ac24fb4872f4af5297ecc3d8ea54b03 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 3 Jun 2026 15:57:21 -0400 Subject: [PATCH 5/5] feat(callbacks): extend Community Apps alpha contracts Purpose of the change - Add the remaining Community Apps iframe contracts needed by CA/Core integration. Previous behavior - Shared callbacks only described installed-app status lookup for Community Apps. - The package subpath exports only declared import/type conditions, which left some dev tooling unable to resolve @unraid/shared-callbacks/client. Why that was a problem - CA and Core need a typed postMessage install request path, richer theme payloads, and explicit install review responses before this can be consumed outside local links. - Resolver failures blocked vue-tsc in consumers using the linked package. What the new change accomplishes - Adds typed Community Apps install bridge actions, requests, and responses. - Allows structured light/dark/shared theme payloads while preserving simple color mode strings. - Exports the new types from client and server entrypoints. - Adds default package export targets for Node/tooling compatibility. How it works - Extends CommunityAppsLaunch with optional installAction and structured theme payloads. - Adds requestInstall to the Community Apps host bridge method contract. - Rebuilds dist declarations and bundles from the source types. --- dist/client.d.ts | 4 ++-- dist/community-apps-client.d.ts | 3 ++- dist/server.d.ts | 4 ++-- dist/types.d.ts | 28 ++++++++++++++++++++++++++- package.json | 9 ++++++--- src/__tests__/server.test.ts | 20 ++++++++++++++++++- src/client.ts | 8 ++++++++ src/community-apps-client.ts | 5 +++++ src/server.ts | 8 ++++++++ src/types.ts | 34 ++++++++++++++++++++++++++++++++- 10 files changed, 112 insertions(+), 11 deletions(-) diff --git a/dist/client.d.ts b/dist/client.d.ts index 1a209d9..b028ddf 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; import { CommunityAppsInstalledAppStatus } from "./types.js"; export { COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, createCommunityAppsInstalledAppHash, isCommunityAppsInstalledAppHash, } from "./community-apps.js"; export { createCommunityAppsInstalledAppsHostBridge, type CommunityAppsInstalledAppsHostBridge, type CommunityAppsInstalledAppsHostMethods, type CreateCommunityAppsInstalledAppsHostBridgeOptions, } from "./community-apps-client.js"; @@ -23,4 +23,4 @@ export declare const useCallback: (config: CallbackConfig) => { generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; export { CommunityAppsInstalledAppStatus }; -export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, WatcherOptions, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/community-apps-client.d.ts b/dist/community-apps-client.d.ts index 70fe67e..a24ee5a 100644 --- a/dist/community-apps-client.d.ts +++ b/dist/community-apps-client.d.ts @@ -1,6 +1,7 @@ -import type { CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse } from "./types.js"; +import type { CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse } from "./types.js"; export type CommunityAppsInstalledAppsHostMethods = { getInstalledAppStatuses: (request: CommunityAppsInstalledAppStatusRequest) => CommunityAppsInstalledAppStatusResponse | Promise; + requestInstall?: (request: CommunityAppsInstallRequest) => CommunityAppsInstallResponse | Promise; }; export type CommunityAppsInstalledAppsHostBridge = { close: () => void; diff --git a/dist/server.d.ts b/dist/server.d.ts index 565b8d8..4a8ee37 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,4 +1,4 @@ -import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; +import type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload } from "./types.js"; import { CommunityAppsInstalledAppStatus } from "./types.js"; export { COMMUNITY_APPS_INSTALLED_APP_HASH_LENGTH, COMMUNITY_APPS_INSTALLED_APPS_ALGORITHM, createCommunityAppsInstalledAppHash, isCommunityAppsInstalledAppHash, } from "./community-apps.js"; /** @@ -14,4 +14,4 @@ export declare const createServerCallback: (config: CallbackConfig) => { generateUrl: (url: string, payload: SendPayloads, sendType?: string, sender?: string) => string; }; export { CommunityAppsInstalledAppStatus }; -export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; +export type { CallbackConfig, QueryPayloads, SendPayloads, SignIn, SignOut, OemSignOut, Troubleshoot, Recover, Replace, TrialExtend, TrialStart, Purchase, Redeem, Renew, Upgrade, UpdateOs, DowngradeOs, Manage, MyKeys, LinkKey, Activate, CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, CommunityAppsInstalledAppsAlgorithm, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/types.d.ts b/dist/types.d.ts index 0680bc0..5e25163 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -117,6 +117,14 @@ export interface CommunityAppsInstalledAppsLookup extends CommunityAppsInstalled mode: "lookup"; } export type CommunityAppsInstalledApps = CommunityAppsInstalledAppsBatch | CommunityAppsInstalledAppsLookup; +export type CommunityAppsThemeColorMode = "system" | "light" | "dark"; +export type CommunityAppsThemeVariables = Record; +export interface CommunityAppsThemePayload { + colorMode?: CommunityAppsThemeColorMode; + light?: CommunityAppsThemeVariables; + dark?: CommunityAppsThemeVariables; + shared?: CommunityAppsThemeVariables; +} export interface CommunityAppsInstalledAppStatusRequest { algorithm: CommunityAppsInstalledAppsAlgorithm; salt: string; @@ -125,16 +133,34 @@ export interface CommunityAppsInstalledAppStatusRequest { export interface CommunityAppsInstalledAppStatusResponse { apps: CommunityAppsInstalledAppStatusMap; } +export type CommunityAppsInstallActionType = "communityApps.installDocker"; +export interface CommunityAppsInstallBridgeAction { + mode: "postMessage"; + method: "requestInstall"; + type: CommunityAppsInstallActionType; +} +export interface CommunityAppsInstallRequest { + type: CommunityAppsInstallActionType; + templateUrl: string; + appId?: string; + appName?: string; +} +export interface CommunityAppsInstallResponse { + accepted: boolean; + reviewRequestId?: string; + reason?: string; +} export interface CommunityAppsLaunch { type: CommunityApps; server: ServerData; installedApps?: CommunityAppsInstalledApps; + installAction?: CommunityAppsInstallBridgeAction; installUrl?: string; installUrlTemplate?: string; installParam?: string; installTarget?: string; path?: string; - theme?: string; + theme?: CommunityAppsThemeColorMode | CommunityAppsThemePayload; locale?: string; } export type ExternalActions = ExternalSignIn | ExternalSignOut | ExternalKeyActions | ExternalUpdateOsAction; diff --git a/package.json b/package.json index 88870b0..a29f195 100644 --- a/package.json +++ b/package.json @@ -15,15 +15,18 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": "./dist/index.js", + "default": "./dist/index.js" }, "./client": { "types": "./dist/client.d.ts", - "import": "./dist/client.js" + "import": "./dist/client.js", + "default": "./dist/client.js" }, "./server": { "types": "./dist/server.d.ts", - "import": "./dist/server.js" + "import": "./dist/server.js", + "default": "./dist/server.js" } }, "files": [ diff --git a/src/__tests__/server.test.ts b/src/__tests__/server.test.ts index 4731bf4..3fddb49 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -94,8 +94,26 @@ describe('createServerCallback (server entry)', () => { mode: 'lookup', salt: 'launch-salt', }, + installAction: { + mode: 'postMessage', + method: 'requestInstall', + type: 'communityApps.installDocker', + }, path: '/apps', - theme: 'dark', + theme: { + colorMode: 'dark', + dark: { + '--color-base-100': '#101014', + '--color-primary': 'oklch(62% 0.21 252)', + }, + light: { + '--color-base-100': '#ffffff', + '--color-primary': '#0066cc', + }, + shared: { + '--radius-box': '0.375rem', + }, + }, }, ] diff --git a/src/client.ts b/src/client.ts index c06f8b6..2793bdd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -45,6 +45,10 @@ import type { CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -256,6 +260,10 @@ export type { CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/community-apps-client.ts b/src/community-apps-client.ts index 6b323f1..af15ad7 100644 --- a/src/community-apps-client.ts +++ b/src/community-apps-client.ts @@ -3,6 +3,8 @@ import { WindowMessenger, } from "post-me"; import type { + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, } from "./types.js"; @@ -13,6 +15,9 @@ export type CommunityAppsInstalledAppsHostMethods = { ) => | CommunityAppsInstalledAppStatusResponse | Promise; + requestInstall?: ( + request: CommunityAppsInstallRequest + ) => CommunityAppsInstallResponse | Promise; }; export type CommunityAppsInstalledAppsHostBridge = { diff --git a/src/server.ts b/src/server.ts index 53cba11..5c30b9e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -44,6 +44,10 @@ import type { CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, @@ -150,6 +154,10 @@ export type { CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, diff --git a/src/types.ts b/src/types.ts index 782d1ea..53bbdd5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -199,6 +199,16 @@ export type CommunityAppsInstalledApps = | CommunityAppsInstalledAppsBatch | CommunityAppsInstalledAppsLookup; +export type CommunityAppsThemeColorMode = "system" | "light" | "dark"; +export type CommunityAppsThemeVariables = Record; + +export interface CommunityAppsThemePayload { + colorMode?: CommunityAppsThemeColorMode; + light?: CommunityAppsThemeVariables; + dark?: CommunityAppsThemeVariables; + shared?: CommunityAppsThemeVariables; +} + export interface CommunityAppsInstalledAppStatusRequest { algorithm: CommunityAppsInstalledAppsAlgorithm; salt: string; @@ -209,16 +219,38 @@ export interface CommunityAppsInstalledAppStatusResponse { apps: CommunityAppsInstalledAppStatusMap; } +export type CommunityAppsInstallActionType = "communityApps.installDocker"; + +export interface CommunityAppsInstallBridgeAction { + mode: "postMessage"; + method: "requestInstall"; + type: CommunityAppsInstallActionType; +} + +export interface CommunityAppsInstallRequest { + type: CommunityAppsInstallActionType; + templateUrl: string; + appId?: string; + appName?: string; +} + +export interface CommunityAppsInstallResponse { + accepted: boolean; + reviewRequestId?: string; + reason?: string; +} + export interface CommunityAppsLaunch { type: CommunityApps; server: ServerData; installedApps?: CommunityAppsInstalledApps; + installAction?: CommunityAppsInstallBridgeAction; installUrl?: string; installUrlTemplate?: string; installParam?: string; installTarget?: string; path?: string; - theme?: string; + theme?: CommunityAppsThemeColorMode | CommunityAppsThemePayload; locale?: string; }