diff --git a/README.md b/README.md index 82185e6..e1d6b7b 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,24 @@ 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 +- `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 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 ```typescript diff --git a/dist/client.d.ts b/dist/client.d.ts index f03b7d2..b028ddf 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -1,4 +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, 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, 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"; 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 +22,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, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, 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, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/client.js b/dist/client.js index a84a995..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,134 @@ 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"; + 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()); @@ -2079,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; @@ -2162,4 +2327,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { 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..a24ee5a --- /dev/null +++ b/dist/community-apps-client.d.ts @@ -0,0 +1,17 @@ +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; +}; +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 a84a995..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,134 @@ 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"; + 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()); @@ -2079,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; @@ -2162,4 +2327,4 @@ var createCallback = (config) => { }; var useCallback = createCallback; -export { 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 3266842..4a8ee37 100644 --- a/dist/server.d.ts +++ b/dist/server.d.ts @@ -1,4 +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, 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, 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"; /** * Server-safe factory that exposes only parse and generateUrl. * @@ -11,4 +13,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, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, ServerActionTypes, ConnectState, ServerState, ServerData, UserInfo, ExternalSignIn, ExternalSignOut, ExternalKeyActions, ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, 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, CommunityAppsInstalledAppsMode, CommunityAppsInstalledAppHash, CommunityAppsInstalledAppStatusMap, CommunityAppsInstalledAppsBatch, CommunityAppsInstalledAppsLookup, CommunityAppsInstalledApps, CommunityAppsInstalledAppStatusRequest, CommunityAppsInstalledAppStatusResponse, CommunityAppsInstallActionType, CommunityAppsInstallBridgeAction, CommunityAppsInstallRequest, CommunityAppsInstallResponse, CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, UpcPayload, }; diff --git a/dist/server.js b/dist/server.js index 1c39717..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,134 @@ 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"; + 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()); @@ -2079,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) => { @@ -2101,4 +2242,4 @@ var createServerCallback = (config) => { }; }; -export { 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 41c313a..5e25163 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,76 @@ export interface ServerTroubleshoot { type: Troubleshoot; 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 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 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; + appHashes: CommunityAppsInstalledAppHash[]; +} +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?: CommunityAppsThemeColorMode | CommunityAppsThemePayload; + 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/package.json b/package.json index 9a922b7..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": [ @@ -41,7 +44,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 65102aa..3fddb49 100644 --- a/src/__tests__/server.test.ts +++ b/src/__tests__/server.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest' -import { createServerCallback } from '../server' -import type { ServerPayload } from '../types' +import { + createCommunityAppsInstalledAppHash, + createServerCallback, +} from '../server' +import type { CommunityAppsLaunch, ServerPayload } from '../types' describe('createServerCallback (server entry)', () => { const config = { @@ -68,4 +71,72 @@ 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', + installedApps: { + enabled: true, + algorithm: 'sha256-128', + mode: 'lookup', + salt: 'launch-salt', + }, + installAction: { + mode: 'postMessage', + method: 'requestInstall', + type: 'communityApps.installDocker', + }, + path: '/apps', + 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', + }, + }, + }, + ] + + 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', + }) + }) + + 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 c3553bb..2793bdd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,6 +21,7 @@ import type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -35,16 +36,43 @@ import type { 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"; import { appendEncryptedDataToUrl, 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; @@ -182,6 +210,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, @@ -206,6 +236,7 @@ export type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -220,6 +251,20 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, + CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, diff --git a/src/community-apps-client.ts b/src/community-apps-client.ts new file mode 100644 index 0000000..af15ad7 --- /dev/null +++ b/src/community-apps-client.ts @@ -0,0 +1,58 @@ +import { + ParentHandshake, + WindowMessenger, +} from "post-me"; +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; +}; + +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 b76e95f..5c30b9e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,6 +20,7 @@ import type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -34,16 +35,37 @@ import type { 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"; import { appendEncryptedDataToUrl, 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. @@ -83,6 +105,8 @@ export const createServerCallback = (config: CallbackConfig) => { }; }; +export { CommunityAppsInstalledAppStatus }; + // Re-export all types for convenience from the server entry. export type { CallbackConfig, @@ -106,6 +130,7 @@ export type { MyKeys, LinkKey, Activate, + CommunityApps, AccountActionTypes, AccountKeyActionTypes, PurchaseActionTypes, @@ -120,6 +145,20 @@ export type { ExternalUpdateOsAction, ServerPayload, ServerTroubleshoot, + CommunityAppsInstalledAppsAlgorithm, + CommunityAppsInstalledAppsMode, + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatusMap, + CommunityAppsInstalledAppsBatch, + CommunityAppsInstalledAppsLookup, + CommunityAppsInstalledApps, + CommunityAppsInstalledAppStatusRequest, + CommunityAppsInstalledAppStatusResponse, + CommunityAppsInstallActionType, + CommunityAppsInstallBridgeAction, + CommunityAppsInstallRequest, + CommunityAppsInstallResponse, + CommunityAppsLaunch, ExternalActions, UpcActions, ExternalPayload, diff --git a/src/types.ts b/src/types.ts index 9e64557..53bbdd5 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,105 @@ export interface ServerTroubleshoot { server: ServerData; } +export type CommunityAppsInstalledAppsAlgorithm = "sha256-128"; +export type CommunityAppsInstalledAppsMode = "batch" | "lookup"; + +export enum CommunityAppsInstalledAppStatus { + Installed = 1, + PreviouslyInstalled = 2, +} + +export type CommunityAppsInstalledAppHash = string; + +export type CommunityAppsInstalledAppStatusMap = Record< + CommunityAppsInstalledAppHash, + CommunityAppsInstalledAppStatus +>; + +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 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; + appHashes: CommunityAppsInstalledAppHash[]; +} + +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?: CommunityAppsThemeColorMode | CommunityAppsThemePayload; + 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[];