@aglyn/plugins-marketplace 1.15.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +1 -0
  2. package/package.json +15 -14
  3. package/src/lib/components/editor-connections-provider.component.d.ts +37 -0
  4. package/src/lib/components/editor-connections-provider.component.js +161 -0
  5. package/src/lib/components/editor-connections-provider.component.js.map +1 -0
  6. package/src/lib/components/listing-connection-credentials-card.component.d.ts +33 -0
  7. package/src/lib/components/listing-connection-credentials-card.component.js +318 -0
  8. package/src/lib/components/listing-connection-credentials-card.component.js.map +1 -0
  9. package/src/lib/components/listing-detail-page.component.js +10 -2
  10. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  11. package/src/lib/components/site-plugin-connections-card.component.d.ts +38 -0
  12. package/src/lib/components/site-plugin-connections-card.component.js +576 -0
  13. package/src/lib/components/site-plugin-connections-card.component.js.map +1 -0
  14. package/src/lib/declarations.console-server.d.ts +17 -0
  15. package/src/lib/declarations.console-server.js +41 -0
  16. package/src/lib/declarations.console-server.js.map +1 -0
  17. package/src/lib/declarations.js +11 -2
  18. package/src/lib/declarations.js.map +1 -1
  19. package/src/lib/declarations.server.js +68 -1
  20. package/src/lib/declarations.server.js.map +1 -1
  21. package/src/lib/hooks/use-plugin-connections-api.d.ts +89 -0
  22. package/src/lib/hooks/use-plugin-connections-api.js +132 -0
  23. package/src/lib/hooks/use-plugin-connections-api.js.map +1 -0
  24. package/src/lib/model/editor-connections.d.ts +46 -0
  25. package/src/lib/model/editor-connections.js +70 -0
  26. package/src/lib/model/editor-connections.js.map +1 -0
  27. package/src/lib/model/listing-price-floor.js +7 -4
  28. package/src/lib/model/listing-price-floor.js.map +1 -1
  29. package/src/lib/model/marketplace-publisher-permissions.d.ts +3 -1
  30. package/src/lib/model/marketplace-publisher-permissions.js +14 -0
  31. package/src/lib/model/marketplace-publisher-permissions.js.map +1 -1
  32. package/src/lib/model/marketplace-site-permissions.d.ts +9 -0
  33. package/src/lib/model/marketplace-site-permissions.js +29 -0
  34. package/src/lib/model/marketplace-site-permissions.js.map +1 -1
  35. package/src/lib/model/plugin-connection-card.d.ts +88 -0
  36. package/src/lib/model/plugin-connection-card.js +187 -0
  37. package/src/lib/model/plugin-connection-card.js.map +1 -0
  38. package/src/lib/model/plugin-connections.d.ts +172 -0
  39. package/src/lib/model/plugin-connections.js +119 -0
  40. package/src/lib/model/plugin-connections.js.map +1 -0
  41. package/src/lib/plugin.js +21 -1
  42. package/src/lib/plugin.js.map +1 -1
  43. package/src/lib/server/admin-reviews.js +18 -12
  44. package/src/lib/server/admin-reviews.js.map +1 -1
  45. package/src/lib/server/artifact-inventory.js +3 -1
  46. package/src/lib/server/artifact-inventory.js.map +1 -1
  47. package/src/lib/server/checkout.js +4 -0
  48. package/src/lib/server/checkout.js.map +1 -1
  49. package/src/lib/server/connections/cleanup.d.ts +84 -0
  50. package/src/lib/server/connections/cleanup.js +116 -0
  51. package/src/lib/server/connections/cleanup.js.map +1 -0
  52. package/src/lib/server/connections/config.d.ts +42 -0
  53. package/src/lib/server/connections/config.js +48 -0
  54. package/src/lib/server/connections/config.js.map +1 -0
  55. package/src/lib/server/connections/connection-store.d.ts +173 -0
  56. package/src/lib/server/connections/connection-store.js +331 -0
  57. package/src/lib/server/connections/connection-store.js.map +1 -0
  58. package/src/lib/server/connections/credentials-store.d.ts +71 -0
  59. package/src/lib/server/connections/credentials-store.js +78 -0
  60. package/src/lib/server/connections/credentials-store.js.map +1 -0
  61. package/src/lib/server/connections/erasure.d.ts +25 -0
  62. package/src/lib/server/connections/erasure.js +72 -0
  63. package/src/lib/server/connections/erasure.js.map +1 -0
  64. package/src/lib/server/connections/installed-connection.d.ts +64 -0
  65. package/src/lib/server/connections/installed-connection.js +92 -0
  66. package/src/lib/server/connections/installed-connection.js.map +1 -0
  67. package/src/lib/server/connections/oauth-state.d.ts +129 -0
  68. package/src/lib/server/connections/oauth-state.js +252 -0
  69. package/src/lib/server/connections/oauth-state.js.map +1 -0
  70. package/src/lib/server/connections/provider-http.d.ts +99 -0
  71. package/src/lib/server/connections/provider-http.js +253 -0
  72. package/src/lib/server/connections/provider-http.js.map +1 -0
  73. package/src/lib/server/connections/record-announce.d.ts +20 -0
  74. package/src/lib/server/connections/record-announce.js +53 -0
  75. package/src/lib/server/connections/record-announce.js.map +1 -0
  76. package/src/lib/server/connections/record-mapping.d.ts +63 -0
  77. package/src/lib/server/connections/record-mapping.js +151 -0
  78. package/src/lib/server/connections/record-mapping.js.map +1 -0
  79. package/src/lib/server/connections/record-routes.d.ts +17 -0
  80. package/src/lib/server/connections/record-routes.js +96 -0
  81. package/src/lib/server/connections/record-routes.js.map +1 -0
  82. package/src/lib/server/connections/revoke.d.ts +29 -0
  83. package/src/lib/server/connections/revoke.js +52 -0
  84. package/src/lib/server/connections/revoke.js.map +1 -0
  85. package/src/lib/server/connections/routes.d.ts +77 -0
  86. package/src/lib/server/connections/routes.js +706 -0
  87. package/src/lib/server/connections/routes.js.map +1 -0
  88. package/src/lib/server/connections/site-records-platform.d.ts +24 -0
  89. package/src/lib/server/connections/site-records-platform.js +94 -0
  90. package/src/lib/server/connections/site-records-platform.js.map +1 -0
  91. package/src/lib/server/connections/site-records.d.ts +95 -0
  92. package/src/lib/server/connections/site-records.js +221 -0
  93. package/src/lib/server/connections/site-records.js.map +1 -0
  94. package/src/lib/server/connections/sync.d.ts +110 -0
  95. package/src/lib/server/connections/sync.js +414 -0
  96. package/src/lib/server/connections/sync.js.map +1 -0
  97. package/src/lib/server/install-plugin.js +56 -6
  98. package/src/lib/server/install-plugin.js.map +1 -1
  99. package/src/lib/server/publish-plugin.js +5 -2
  100. package/src/lib/server/publish-plugin.js.map +1 -1
  101. package/src/lib/server/revenue-source.js +6 -5
  102. package/src/lib/server/revenue-source.js.map +1 -1
  103. package/src/lib/server.js +60 -0
  104. package/src/lib/server.js.map +1 -1
  105. package/src/lib/testing/memory-firestore.d.ts +34 -0
  106. package/src/lib/testing/memory-firestore.js +250 -0
  107. package/src/lib/testing/memory-firestore.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/provider-http.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n preparePluginConnectionRequest,\n readPluginConnectionPath,\n type PluginConnectionDeclaration,\n type PluginConnectionRequest,\n type PluginConnectionResource,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport type { PluginConnectionChoice } from '../../model/plugin-connections'\n\n/**\n * HOW THE PLATFORM TALKS TO A DECLARED CONNECTION'S PROVIDER (AGL-4193).\n *\n * Every outbound request a connection causes goes through here: the code\n * exchange and the refresh at the declared `tokenUrl`, the revoke at the\n * declared `revocationUrl`, and the templated API requests, which\n * `preparePluginConnectionRequest` refuses to send anywhere but the declared\n * `apiHosts`. One runtime object holds the fetch, the clock and the sleep, so\n * a spec swaps them in one place and no request leaves the process.\n *\n * Redirects are NOT followed: a provider answering a token or API request\n * with a redirect would otherwise carry the grant to a host nobody declared.\n * Answers are read up to a ceiling, so a hostile or broken endpoint cannot\n * make the console buffer an unbounded body.\n */\n\nexport interface PluginConnectionRuntime {\n fetch: typeof fetch\n now: () => number\n sleep: (ms: number) => Promise<void>\n}\n\nexport const pluginConnectionRuntime: PluginConnectionRuntime = {\n fetch: (input, init) => globalThis.fetch(input, init),\n now: () => Date.now(),\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n}\n\n/** Why a provider call failed: the grant is gone (`auth`), try later (`transient`), or the answer is unusable (`refused`). */\nexport type PluginConnectionErrorCode = 'auth' | 'transient' | 'refused'\n\nexport class PluginConnectionProviderError extends Error {\n constructor(\n readonly code: PluginConnectionErrorCode,\n message: string,\n readonly status = 0,\n ) {\n super(message)\n this.name = 'PluginConnectionProviderError'\n }\n}\n\n/** The publisher's OAuth app, opened from its sealed record. */\nexport interface PluginConnectionClient {\n clientId: string\n clientSecret: string\n}\n\n/** A token endpoint's answer, normalized. */\nexport interface PluginConnectionTokenSet {\n accessToken: string\n /** `null` when the provider issued none (or kept the one it issued before). */\n refreshToken: string | null\n /** Epoch ms; an hour from now when the provider did not say. */\n accessExpiresAtMs: number\n /** The scopes granted, when the provider said; else the ones asked for. */\n scopes: string[]\n}\n\n/** Bytes of an answer read at most. */\nexport const PLUGIN_CONNECTION_MAX_RESPONSE_BYTES = 2 * 1024 * 1024\nconst ATTEMPT_TIMEOUT_MS = 30_000\nconst RETRIES = 2\nconst BASE_BACKOFF_MS = 500\nconst DEFAULT_ACCESS_LIFETIME_MS = 60 * 60 * 1000\n\n/** Reads an answer's body, refusing one past the ceiling. */\nasync function readBody(response: Response): Promise<string> {\n const declared = Number(response.headers.get('content-length'))\n if (Number.isFinite(declared) && declared > PLUGIN_CONNECTION_MAX_RESPONSE_BYTES) {\n throw new PluginConnectionProviderError('refused', 'The provider’s answer is too large.', response.status)\n }\n const text = await response.text()\n if (text.length > PLUGIN_CONNECTION_MAX_RESPONSE_BYTES) {\n throw new PluginConnectionProviderError('refused', 'The provider’s answer is too large.', response.status)\n }\n return text\n}\n\nconst parseJson = (text: string): unknown => {\n try {\n return JSON.parse(text)\n } catch {\n return null\n }\n}\n\n/** The provider's own sentence for a refusal, cut short; never a token. */\nfunction providerSentence(body: unknown, status: number): string {\n if (body && typeof body === 'object') {\n const record = body as Record<string, unknown>\n if (typeof record['error_description'] === 'string') return record['error_description'].slice(0, 300)\n const error = record['error']\n if (typeof error === 'string') return error.slice(0, 300)\n if (error && typeof error === 'object' && typeof (error as Record<string, unknown>)['message'] === 'string') {\n return String((error as Record<string, unknown>)['message']).slice(0, 300)\n }\n }\n return `HTTP ${status}`\n}\n\nconst retryable = (status: number) => status === 429 || status >= 500\n\n/** One request: no redirects, a timeout per attempt, a throttle or server error retried. */\nasync function send(url: string, init: RequestInit): Promise<{ status: number; body: unknown }> {\n let lastError: unknown = null\n for (let attempt = 0; attempt <= RETRIES; attempt += 1) {\n let response: Response | null = null\n try {\n response = await pluginConnectionRuntime.fetch(url, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS),\n })\n } catch (error) {\n lastError = error\n }\n if (response && !retryable(response.status)) {\n if (response.status >= 300 && response.status < 400) {\n throw new PluginConnectionProviderError('refused', 'The provider answered with a redirect, which is not followed.', response.status)\n }\n return { status: response.status, body: parseJson(await readBody(response)) }\n }\n if (attempt === RETRIES) {\n if (response) {\n const body = parseJson(await readBody(response).catch(() => ''))\n throw new PluginConnectionProviderError('transient', providerSentence(body, response.status), response.status)\n }\n break\n }\n await pluginConnectionRuntime.sleep(BASE_BACKOFF_MS * 2 ** attempt)\n }\n throw new PluginConnectionProviderError(\n 'transient',\n `The provider could not be reached: ${String((lastError as Error)?.message ?? lastError).slice(0, 200)}`,\n )\n}\n\n/** The token endpoint's answer, or the refusal: `invalid_grant` and a 401 mean the grant is gone. */\nfunction readTokenAnswer(\n answer: { status: number; body: unknown },\n connection: PluginConnectionDeclaration,\n): PluginConnectionTokenSet {\n const body = (answer.body && typeof answer.body === 'object' ? answer.body : {}) as Record<string, unknown>\n if (answer.status < 200 || answer.status >= 300 || typeof body['access_token'] !== 'string' || !body['access_token']) {\n const sentence = providerSentence(body, answer.status)\n const grantGone = body['error'] === 'invalid_grant' || answer.status === 401 || body['error'] === 'unauthorized_client'\n throw new PluginConnectionProviderError(grantGone ? 'auth' : 'refused', sentence, answer.status)\n }\n const expiresIn = Number(body['expires_in'])\n const scope = typeof body['scope'] === 'string' ? body['scope'] : ''\n return {\n accessToken: body['access_token'],\n refreshToken: typeof body['refresh_token'] === 'string' && body['refresh_token'] ? body['refresh_token'] : null,\n accessExpiresAtMs:\n pluginConnectionRuntime.now() +\n (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1000 : DEFAULT_ACCESS_LIFETIME_MS),\n scopes: scope ? scope.split(/[\\s,]+/).filter(Boolean) : [...connection.scopes],\n }\n}\n\n/** Posts a form to the declared token endpoint, authenticating the publisher's app as declared. */\nasync function postToken(\n connection: PluginConnectionDeclaration,\n client: PluginConnectionClient,\n fields: Record<string, string>,\n): Promise<{ status: number; body: unknown }> {\n const form = new URLSearchParams(fields)\n const headers: Record<string, string> = {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n }\n if (connection.tokenAuthMethod === 'basic') {\n const pair = `${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret)}`\n headers['Authorization'] = `Basic ${Buffer.from(pair, 'utf8').toString('base64')}`\n } else {\n form.set('client_id', client.clientId)\n form.set('client_secret', client.clientSecret)\n }\n return send(connection.tokenUrl, { method: 'POST', headers, body: form.toString() })\n}\n\n/** Exchanges an authorization code at the declared token endpoint. */\nexport async function exchangePluginConnectionCode(\n connection: PluginConnectionDeclaration,\n client: PluginConnectionClient,\n input: { code: string; redirectUri: string },\n): Promise<PluginConnectionTokenSet> {\n const answer = await postToken(connection, client, {\n grant_type: 'authorization_code',\n code: input.code,\n redirect_uri: input.redirectUri,\n })\n return readTokenAnswer(answer, connection)\n}\n\n/** Refreshes a grant at the declared token endpoint. */\nexport async function refreshPluginConnectionToken(\n connection: PluginConnectionDeclaration,\n client: PluginConnectionClient,\n refreshToken: string,\n): Promise<PluginConnectionTokenSet> {\n const answer = await postToken(connection, client, { grant_type: 'refresh_token', refresh_token: refreshToken })\n return readTokenAnswer(answer, connection)\n}\n\n/** Revokes a grant at the declared revocation endpoint, when there is one. Best effort; throws on failure. */\nexport async function revokePluginConnectionToken(\n connection: PluginConnectionDeclaration,\n client: PluginConnectionClient | null,\n token: string,\n): Promise<boolean> {\n if (!connection.revocationUrl) return false\n const form = new URLSearchParams({ token })\n if (client && connection.tokenAuthMethod !== 'basic') {\n form.set('client_id', client.clientId)\n form.set('client_secret', client.clientSecret)\n }\n const headers: Record<string, string> = { 'Content-Type': 'application/x-www-form-urlencoded' }\n if (client && connection.tokenAuthMethod === 'basic') {\n const pair = `${encodeURIComponent(client.clientId)}:${encodeURIComponent(client.clientSecret)}`\n headers['Authorization'] = `Basic ${Buffer.from(pair, 'utf8').toString('base64')}`\n }\n const answer = await send(connection.revocationUrl, { method: 'POST', headers, body: form.toString() })\n if (answer.status >= 200 && answer.status < 300) return true\n throw new PluginConnectionProviderError('refused', providerSentence(answer.body, answer.status), answer.status)\n}\n\n/**\n * One templated API request with the site's access token, filled from its\n * choices; answers the parsed JSON. A 401 or 403 is `auth` (the caller may\n * refresh once and retry), anything else not 2xx is `refused`.\n */\nexport async function pluginConnectionApiRequest(\n connection: PluginConnectionDeclaration,\n request: PluginConnectionRequest,\n input: { accessToken: string; choices: Readonly<Record<string, string>>; cursor?: string | null },\n): Promise<unknown> {\n const prepared = preparePluginConnectionRequest(request, {\n apiHosts: connection.apiHosts,\n choices: input.choices,\n cursor: input.cursor ?? null,\n })\n if (prepared.ok === false) throw new PluginConnectionProviderError('refused', prepared.error)\n const headers: Record<string, string> = {\n Authorization: `Bearer ${input.accessToken}`,\n Accept: 'application/json',\n }\n if (prepared.request.body !== undefined) headers['Content-Type'] = 'application/json'\n const answer = await send(prepared.request.url, {\n method: prepared.request.method,\n headers,\n ...(prepared.request.body !== undefined ? { body: prepared.request.body } : {}),\n })\n if (answer.status === 401 || answer.status === 403) {\n throw new PluginConnectionProviderError('auth', providerSentence(answer.body, answer.status), answer.status)\n }\n if (answer.status < 200 || answer.status >= 300) {\n throw new PluginConnectionProviderError('refused', providerSentence(answer.body, answer.status), answer.status)\n }\n return answer.body\n}\n\n/**\n * Every item of a paginated request, up to `maxItems` and the declared page\n * ceiling. Exported for the sync, which reads its requests the same way.\n */\nexport async function pluginConnectionApiItems(\n connection: PluginConnectionDeclaration,\n request: PluginConnectionRequest,\n input: { accessToken: string; choices: Readonly<Record<string, string>>; maxItems: number },\n): Promise<{ items: unknown[]; pages: unknown[] }> {\n const items: unknown[] = []\n const pages: unknown[] = []\n let cursor: string | null = null\n const maxPages = request.pagination?.maxPages ?? 1\n for (let page = 0; page < maxPages; page += 1) {\n const body = await pluginConnectionApiRequest(connection, request, { ...input, cursor })\n pages.push(body)\n const list = request.itemsPath ? readPluginConnectionPath(body, request.itemsPath) : undefined\n if (Array.isArray(list)) items.push(...list.slice(0, Math.max(0, input.maxItems - items.length)))\n if (items.length >= input.maxItems || !request.pagination) break\n const next = readPluginConnectionPath(body, request.pagination.cursorPath)\n if (typeof next !== 'string' || !next || next.length > 2048 || next === cursor) break\n cursor = next\n }\n return { items, pages }\n}\n\n/** The resource's items as the picker shows them: an id and a label, each a plain bounded string. */\nexport async function listPluginConnectionResource(\n connection: PluginConnectionDeclaration,\n resource: PluginConnectionResource,\n input: { accessToken: string; choices: Readonly<Record<string, string>> },\n): Promise<PluginConnectionChoice[]> {\n const { items } = await pluginConnectionApiItems(connection, resource.request, {\n ...input,\n maxItems: resource.maxItems,\n })\n const out: PluginConnectionChoice[] = []\n const seen = new Set<string>()\n for (const item of items) {\n const id = readPluginConnectionPath(item, resource.idPath)\n const label = readPluginConnectionPath(item, resource.labelPath)\n if (typeof id !== 'string' && typeof id !== 'number') continue\n const idText = String(id).slice(0, 256)\n if (!idText || seen.has(idText)) continue\n seen.add(idText)\n out.push({ id: idText, label: typeof label === 'string' && label.trim() ? label.trim().slice(0, 200) : idText })\n }\n return out\n}\n"],"names":["preparePluginConnectionRequest","readPluginConnectionPath","pluginConnectionRuntime","fetch","input","init","globalThis","now","Date","sleep","ms","Promise","resolve","setTimeout","PluginConnectionProviderError","Error","code","message","status","name","PLUGIN_CONNECTION_MAX_RESPONSE_BYTES","ATTEMPT_TIMEOUT_MS","RETRIES","BASE_BACKOFF_MS","DEFAULT_ACCESS_LIFETIME_MS","readBody","response","declared","Number","headers","get","isFinite","text","length","parseJson","JSON","parse","providerSentence","body","record","slice","error","String","retryable","send","url","lastError","attempt","redirect","signal","AbortSignal","timeout","catch","readTokenAnswer","answer","connection","sentence","grantGone","expiresIn","scope","accessToken","refreshToken","accessExpiresAtMs","scopes","split","filter","Boolean","postToken","client","fields","form","URLSearchParams","Accept","tokenAuthMethod","pair","encodeURIComponent","clientId","clientSecret","Buffer","from","toString","set","tokenUrl","method","exchangePluginConnectionCode","grant_type","redirect_uri","redirectUri","refreshPluginConnectionToken","refresh_token","revokePluginConnectionToken","token","revocationUrl","pluginConnectionApiRequest","request","prepared","apiHosts","choices","cursor","ok","Authorization","undefined","pluginConnectionApiItems","items","pages","maxPages","pagination","page","push","list","itemsPath","Array","isArray","Math","max","maxItems","next","cursorPath","listPluginConnectionResource","resource","out","seen","Set","item","id","idPath","label","labelPath","idText","has","add","trim"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** Whether a record's shown content differs between two reads (`syncedAtMs` is not content). */
18
+ export declare function connectionRecordChanged(before: Record<string, unknown> | null | undefined, after: Record<string, unknown> | null | undefined): boolean;
19
+ /** Tells a site's pages that one of its connection records changed or went. */
20
+ export declare function announceConnectionRecordChange(hostId: string, reason: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
17
+ /*
18
+ * A SITE'S PAGES HEAR WHEN A CONNECTION'S RECORD CHANGES (AGL-4195).
19
+ *
20
+ * The published page reads a site's records through the tenant's render
21
+ * cache (`site-records-platform.ts`), tagged like the rest of the site's
22
+ * data and kept an hour, so a record that changes must say so: a deleted one
23
+ * (disconnect, uninstall, a grant gone mid-sync) must leave the page at once,
24
+ * and a sync that wrote new values should show them. A sync that wrote what
25
+ * was already there says nothing, so an hourly sync does not regenerate every
26
+ * page of the site every hour.
27
+ *
28
+ * Best effort and never throws: the write it follows has already happened.
29
+ */ const shape = (data)=>{
30
+ var _data_version, _data_fields, _data_items;
31
+ return data ? JSON.stringify([
32
+ (_data_version = data['version']) != null ? _data_version : null,
33
+ (_data_fields = data['fields']) != null ? _data_fields : null,
34
+ (_data_items = data['items']) != null ? _data_items : null
35
+ ]) : null;
36
+ };
37
+ /** Whether a record's shown content differs between two reads (`syncedAtMs` is not content). */ export function connectionRecordChanged(before, after) {
38
+ return shape(before) !== shape(after);
39
+ }
40
+ /** Tells a site's pages that one of its connection records changed or went. */ export async function announceConnectionRecordChange(hostId, reason) {
41
+ try {
42
+ await dropPluginSiteCache({
43
+ hostIds: [
44
+ hostId
45
+ ],
46
+ reason
47
+ });
48
+ } catch (error) {
49
+ console.error('[marketplace] a site was not told its connection record changed', error);
50
+ }
51
+ }
52
+
53
+ //# sourceMappingURL=record-announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/record-announce.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\n\n/*\n * A SITE'S PAGES HEAR WHEN A CONNECTION'S RECORD CHANGES (AGL-4195).\n *\n * The published page reads a site's records through the tenant's render\n * cache (`site-records-platform.ts`), tagged like the rest of the site's\n * data and kept an hour, so a record that changes must say so: a deleted one\n * (disconnect, uninstall, a grant gone mid-sync) must leave the page at once,\n * and a sync that wrote new values should show them. A sync that wrote what\n * was already there says nothing, so an hourly sync does not regenerate every\n * page of the site every hour.\n *\n * Best effort and never throws: the write it follows has already happened.\n */\n\nconst shape = (data: Record<string, unknown> | null | undefined) =>\n data ? JSON.stringify([data['version'] ?? null, data['fields'] ?? null, data['items'] ?? null]) : null\n\n/** Whether a record's shown content differs between two reads (`syncedAtMs` is not content). */\nexport function connectionRecordChanged(\n before: Record<string, unknown> | null | undefined,\n after: Record<string, unknown> | null | undefined,\n): boolean {\n return shape(before) !== shape(after)\n}\n\n/** Tells a site's pages that one of its connection records changed or went. */\nexport async function announceConnectionRecordChange(hostId: string, reason: string): Promise<void> {\n try {\n await dropPluginSiteCache({ hostIds: [hostId], reason })\n } catch (error) {\n console.error('[marketplace] a site was not told its connection record changed', error)\n }\n}\n"],"names":["dropPluginSiteCache","shape","data","JSON","stringify","connectionRecordChanged","before","after","announceConnectionRecordChange","hostId","reason","hostIds","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,gDAA+C;AAEnF;;;;;;;;;;;;CAYC,GAED,MAAMC,QAAQ,CAACC;QACUA,eAAyBA,cAAwBA;WAAxEA,OAAOC,KAAKC,SAAS,CAAC;SAACF,gBAAAA,IAAI,CAAC,UAAU,YAAfA,gBAAmB;SAAMA,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;SAAMA,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB;KAAK,IAAI;;AAEpG,8FAA8F,GAC9F,OAAO,SAASG,wBACdC,MAAkD,EAClDC,KAAiD;IAEjD,OAAON,MAAMK,YAAYL,MAAMM;AACjC;AAEA,6EAA6E,GAC7E,OAAO,eAAeC,+BAA+BC,MAAc,EAAEC,MAAc;IACjF,IAAI;QACF,MAAMV,oBAAoB;YAAEW,SAAS;gBAACF;aAAO;YAAEC;QAAO;IACxD,EAAE,OAAOE,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;IACnF;AACF"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginConnectionRecordField, type PluginConnectionSync } from '@aglyn/aglyn/app-utils/plugin-connections';
18
+ import type { PluginConnectionRecordDoc, PluginConnectionRecordValue } from '../../model/plugin-connections';
19
+ /**
20
+ * HOW A SYNC'S ANSWERS BECOME A SITE'S RECORD (AGL-4194).
21
+ *
22
+ * The provider's answers are untrusted JSON. The record a page renders from
23
+ * is built field by field from the manifest's `sync.record`, so it can only
24
+ * ever hold what was declared, typed as declared and no longer than declared:
25
+ *
26
+ * - a key the schema does not name is never read, so nothing undeclared
27
+ * can be copied across, whatever the provider adds to its answer;
28
+ * - a `map` is a whitelist: a value it does not list is dropped, never
29
+ * passed through raw;
30
+ * - a `string` is cut to its `maxLength`; a `url` must be `https` with no
31
+ * credentials and fit its `maxLength` whole (a cut URL points somewhere
32
+ * else, so a long one is dropped rather than cut);
33
+ * - a `datetime` becomes epoch ms; a `number` must be finite;
34
+ * - a declared field with no usable value is `null`, so every entry has the
35
+ * same keys and an element can tell "missing" from "zero";
36
+ * - `items` stops at its `maxItems`, and the whole record stops at
37
+ * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}, dropping trailing items, so
38
+ * it always fits in one Firestore document with room to spare.
39
+ *
40
+ * Pure: no Firestore, no fetch.
41
+ */
42
+ /** The serialized record is kept under this, well inside Firestore's 1 MiB document limit. */
43
+ export declare const PLUGIN_CONNECTION_RECORD_MAX_BYTES: number;
44
+ /** What one declared sync request answered: its first page (summary fields) and its items across pages. */
45
+ export interface PluginConnectionSyncAnswer {
46
+ pages: unknown[];
47
+ items: unknown[];
48
+ }
49
+ /** One declared field's value out of `source`, or `null` when there is no usable one. */
50
+ export declare function mapPluginConnectionField(field: PluginConnectionRecordField, source: unknown): PluginConnectionRecordValue;
51
+ /**
52
+ * The record for one sync: the summary `fields` from each named request's
53
+ * first page, the `items` from the items request, capped to `maxItems` and to
54
+ * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}.
55
+ */
56
+ export declare function mapPluginConnectionRecord(sync: Pick<PluginConnectionSync, 'record'>, answers: Readonly<Record<string, PluginConnectionSyncAnswer>>, meta: {
57
+ listingId: string;
58
+ connectionId: string;
59
+ version: string;
60
+ syncedAtMs: number;
61
+ }): PluginConnectionRecordDoc;
62
+ /** The sync requests the record actually reads, in declared order: an unread one is never sent. */
63
+ export declare function pluginConnectionSyncRequestsRead(sync: PluginConnectionSync): PluginConnectionSync['requests'];
@@ -0,0 +1,151 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { readPluginConnectionPath } from "@aglyn/aglyn/app-utils/plugin-connections";
18
+ /**
19
+ * HOW A SYNC'S ANSWERS BECOME A SITE'S RECORD (AGL-4194).
20
+ *
21
+ * The provider's answers are untrusted JSON. The record a page renders from
22
+ * is built field by field from the manifest's `sync.record`, so it can only
23
+ * ever hold what was declared, typed as declared and no longer than declared:
24
+ *
25
+ * - a key the schema does not name is never read, so nothing undeclared
26
+ * can be copied across, whatever the provider adds to its answer;
27
+ * - a `map` is a whitelist: a value it does not list is dropped, never
28
+ * passed through raw;
29
+ * - a `string` is cut to its `maxLength`; a `url` must be `https` with no
30
+ * credentials and fit its `maxLength` whole (a cut URL points somewhere
31
+ * else, so a long one is dropped rather than cut);
32
+ * - a `datetime` becomes epoch ms; a `number` must be finite;
33
+ * - a declared field with no usable value is `null`, so every entry has the
34
+ * same keys and an element can tell "missing" from "zero";
35
+ * - `items` stops at its `maxItems`, and the whole record stops at
36
+ * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}, dropping trailing items, so
37
+ * it always fits in one Firestore document with room to spare.
38
+ *
39
+ * Pure: no Firestore, no fetch.
40
+ */ /** The serialized record is kept under this, well inside Firestore's 1 MiB document limit. */ export const PLUGIN_CONNECTION_RECORD_MAX_BYTES = 512 * 1024;
41
+ /** Dates outside these bounds are not dates a provider meant (year 1970 to 2200). */ const MIN_DATETIME_MS = 0;
42
+ const MAX_DATETIME_MS = Date.UTC(2200, 0, 1);
43
+ function coerceUrl(value, maxLength) {
44
+ if (typeof value !== 'string') return null;
45
+ const trimmed = value.trim();
46
+ if (!trimmed || trimmed.length > maxLength) return null;
47
+ let parsed;
48
+ try {
49
+ parsed = new URL(trimmed);
50
+ } catch (unused) {
51
+ return null;
52
+ }
53
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null;
54
+ return trimmed;
55
+ }
56
+ function coerceDatetime(value) {
57
+ let ms;
58
+ if (typeof value === 'number') ms = value;
59
+ else if (typeof value === 'string' && value.trim()) ms = Date.parse(value.trim());
60
+ else return null;
61
+ if (!Number.isFinite(ms) || ms < MIN_DATETIME_MS || ms > MAX_DATETIME_MS) return null;
62
+ return Math.trunc(ms);
63
+ }
64
+ /** One declared field's value out of `source`, or `null` when there is no usable one. */ export function mapPluginConnectionField(field, source) {
65
+ let value = readPluginConnectionPath(source, field.path);
66
+ if (value === undefined || value === null) return null;
67
+ if (field.map) {
68
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return null;
69
+ const key = String(value);
70
+ if (!Object.prototype.hasOwnProperty.call(field.map, key)) return null;
71
+ value = field.map[key];
72
+ }
73
+ switch(field.type){
74
+ case 'string':
75
+ {
76
+ var _field_maxLength;
77
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return null;
78
+ const text = String(value);
79
+ return text.slice(0, (_field_maxLength = field.maxLength) != null ? _field_maxLength : 0);
80
+ }
81
+ case 'number':
82
+ {
83
+ const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) : NaN;
84
+ return Number.isFinite(number) ? number : null;
85
+ }
86
+ case 'boolean':
87
+ if (typeof value === 'boolean') return value;
88
+ if (value === 'true') return true;
89
+ if (value === 'false') return false;
90
+ return null;
91
+ case 'datetime':
92
+ return coerceDatetime(value);
93
+ case 'url':
94
+ var _field_maxLength1;
95
+ return coerceUrl(value, (_field_maxLength1 = field.maxLength) != null ? _field_maxLength1 : 0);
96
+ default:
97
+ return null;
98
+ }
99
+ }
100
+ function mapFields(fields, source) {
101
+ const out = {};
102
+ for (const [name, field] of Object.entries(fields))out[name] = mapPluginConnectionField(field, source);
103
+ return out;
104
+ }
105
+ const byteLength = (value)=>Buffer.byteLength(JSON.stringify(value), 'utf8');
106
+ /**
107
+ * The record for one sync: the summary `fields` from each named request's
108
+ * first page, the `items` from the items request, capped to `maxItems` and to
109
+ * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}.
110
+ */ export function mapPluginConnectionRecord(sync, answers, meta) {
111
+ var _sync_record_fields;
112
+ const summary = (_sync_record_fields = sync.record.fields) != null ? _sync_record_fields : {};
113
+ const fields = {};
114
+ for (const [name, field] of Object.entries(summary)){
115
+ var _answers_field_request;
116
+ fields[name] = mapPluginConnectionField(field, (_answers_field_request = answers[field.request]) == null ? void 0 : _answers_field_request.pages[0]);
117
+ }
118
+ const items = [];
119
+ const declared = sync.record.items;
120
+ if (declared) {
121
+ var _ref;
122
+ var _answers_declared_request;
123
+ const source = (_ref = (_answers_declared_request = answers[declared.request]) == null ? void 0 : _answers_declared_request.items) != null ? _ref : [];
124
+ for (const item of source.slice(0, declared.maxItems)){
125
+ items.push(mapFields(declared.fields, item));
126
+ }
127
+ }
128
+ const record = _extends({
129
+ kind: 'record'
130
+ }, meta, {
131
+ fields,
132
+ items
133
+ });
134
+ // The byte ceiling: drop trailing items until the whole record fits.
135
+ while(record.items.length && byteLength(record) > PLUGIN_CONNECTION_RECORD_MAX_BYTES){
136
+ const over = byteLength(record) - PLUGIN_CONNECTION_RECORD_MAX_BYTES;
137
+ const last = byteLength(record.items[record.items.length - 1]);
138
+ const drop = Math.min(record.items.length, Math.max(1, Math.ceil(over / Math.max(1, last))));
139
+ record.items.splice(record.items.length - drop);
140
+ }
141
+ return record;
142
+ }
143
+ /** The sync requests the record actually reads, in declared order: an unread one is never sent. */ export function pluginConnectionSyncRequestsRead(sync) {
144
+ var _sync_record_fields;
145
+ const read = new Set();
146
+ for (const field of Object.values((_sync_record_fields = sync.record.fields) != null ? _sync_record_fields : {}))read.add(field.request);
147
+ if (sync.record.items) read.add(sync.record.items.request);
148
+ return sync.requests.filter((request)=>read.has(request.id));
149
+ }
150
+
151
+ //# sourceMappingURL=record-mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/record-mapping.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n readPluginConnectionPath,\n type PluginConnectionRecordField,\n type PluginConnectionSync,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport type { PluginConnectionRecordDoc, PluginConnectionRecordValue } from '../../model/plugin-connections'\n\n/**\n * HOW A SYNC'S ANSWERS BECOME A SITE'S RECORD (AGL-4194).\n *\n * The provider's answers are untrusted JSON. The record a page renders from\n * is built field by field from the manifest's `sync.record`, so it can only\n * ever hold what was declared, typed as declared and no longer than declared:\n *\n * - a key the schema does not name is never read, so nothing undeclared\n * can be copied across, whatever the provider adds to its answer;\n * - a `map` is a whitelist: a value it does not list is dropped, never\n * passed through raw;\n * - a `string` is cut to its `maxLength`; a `url` must be `https` with no\n * credentials and fit its `maxLength` whole (a cut URL points somewhere\n * else, so a long one is dropped rather than cut);\n * - a `datetime` becomes epoch ms; a `number` must be finite;\n * - a declared field with no usable value is `null`, so every entry has the\n * same keys and an element can tell \"missing\" from \"zero\";\n * - `items` stops at its `maxItems`, and the whole record stops at\n * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}, dropping trailing items, so\n * it always fits in one Firestore document with room to spare.\n *\n * Pure: no Firestore, no fetch.\n */\n\n/** The serialized record is kept under this, well inside Firestore's 1 MiB document limit. */\nexport const PLUGIN_CONNECTION_RECORD_MAX_BYTES = 512 * 1024\n\n/** What one declared sync request answered: its first page (summary fields) and its items across pages. */\nexport interface PluginConnectionSyncAnswer {\n pages: unknown[]\n items: unknown[]\n}\n\n/** Dates outside these bounds are not dates a provider meant (year 1970 to 2200). */\nconst MIN_DATETIME_MS = 0\nconst MAX_DATETIME_MS = Date.UTC(2200, 0, 1)\n\nfunction coerceUrl(value: unknown, maxLength: number): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed || trimmed.length > maxLength) return null\n let parsed: URL\n try {\n parsed = new URL(trimmed)\n } catch {\n return null\n }\n if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null\n return trimmed\n}\n\nfunction coerceDatetime(value: unknown): number | null {\n let ms: number\n if (typeof value === 'number') ms = value\n else if (typeof value === 'string' && value.trim()) ms = Date.parse(value.trim())\n else return null\n if (!Number.isFinite(ms) || ms < MIN_DATETIME_MS || ms > MAX_DATETIME_MS) return null\n return Math.trunc(ms)\n}\n\n/** One declared field's value out of `source`, or `null` when there is no usable one. */\nexport function mapPluginConnectionField(\n field: PluginConnectionRecordField,\n source: unknown,\n): PluginConnectionRecordValue {\n let value: unknown = readPluginConnectionPath(source, field.path)\n if (value === undefined || value === null) return null\n if (field.map) {\n if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return null\n const key = String(value)\n if (!Object.prototype.hasOwnProperty.call(field.map, key)) return null\n value = field.map[key]\n }\n switch (field.type) {\n case 'string': {\n if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') return null\n const text = String(value)\n return text.slice(0, field.maxLength ?? 0)\n }\n case 'number': {\n const number = typeof value === 'number' ? value : typeof value === 'string' && value.trim() ? Number(value) : NaN\n return Number.isFinite(number) ? number : null\n }\n case 'boolean':\n if (typeof value === 'boolean') return value\n if (value === 'true') return true\n if (value === 'false') return false\n return null\n case 'datetime':\n return coerceDatetime(value)\n case 'url':\n return coerceUrl(value, field.maxLength ?? 0)\n default:\n return null\n }\n}\n\nfunction mapFields(\n fields: Readonly<Record<string, PluginConnectionRecordField>>,\n source: unknown,\n): Record<string, PluginConnectionRecordValue> {\n const out: Record<string, PluginConnectionRecordValue> = {}\n for (const [name, field] of Object.entries(fields)) out[name] = mapPluginConnectionField(field, source)\n return out\n}\n\nconst byteLength = (value: unknown) => Buffer.byteLength(JSON.stringify(value), 'utf8')\n\n/**\n * The record for one sync: the summary `fields` from each named request's\n * first page, the `items` from the items request, capped to `maxItems` and to\n * {@link PLUGIN_CONNECTION_RECORD_MAX_BYTES}.\n */\nexport function mapPluginConnectionRecord(\n sync: Pick<PluginConnectionSync, 'record'>,\n answers: Readonly<Record<string, PluginConnectionSyncAnswer>>,\n meta: { listingId: string; connectionId: string; version: string; syncedAtMs: number },\n): PluginConnectionRecordDoc {\n const summary = sync.record.fields ?? {}\n const fields: Record<string, PluginConnectionRecordValue> = {}\n for (const [name, field] of Object.entries(summary)) {\n fields[name] = mapPluginConnectionField(field, answers[field.request]?.pages[0])\n }\n const items: Array<Record<string, PluginConnectionRecordValue>> = []\n const declared = sync.record.items\n if (declared) {\n const source = answers[declared.request]?.items ?? []\n for (const item of source.slice(0, declared.maxItems)) {\n items.push(mapFields(declared.fields, item))\n }\n }\n const record: PluginConnectionRecordDoc = { kind: 'record', ...meta, fields, items }\n // The byte ceiling: drop trailing items until the whole record fits.\n while (record.items.length && byteLength(record) > PLUGIN_CONNECTION_RECORD_MAX_BYTES) {\n const over = byteLength(record) - PLUGIN_CONNECTION_RECORD_MAX_BYTES\n const last = byteLength(record.items[record.items.length - 1])\n const drop = Math.min(record.items.length, Math.max(1, Math.ceil(over / Math.max(1, last))))\n record.items.splice(record.items.length - drop)\n }\n return record\n}\n\n/** The sync requests the record actually reads, in declared order: an unread one is never sent. */\nexport function pluginConnectionSyncRequestsRead(sync: PluginConnectionSync): PluginConnectionSync['requests'] {\n const read = new Set<string>()\n for (const field of Object.values(sync.record.fields ?? {})) read.add(field.request)\n if (sync.record.items) read.add(sync.record.items.request)\n return sync.requests.filter((request) => read.has(request.id))\n}\n"],"names":["readPluginConnectionPath","PLUGIN_CONNECTION_RECORD_MAX_BYTES","MIN_DATETIME_MS","MAX_DATETIME_MS","Date","UTC","coerceUrl","value","maxLength","trimmed","trim","length","parsed","URL","protocol","username","password","coerceDatetime","ms","parse","Number","isFinite","Math","trunc","mapPluginConnectionField","field","source","path","undefined","map","key","String","Object","prototype","hasOwnProperty","call","type","text","slice","number","NaN","mapFields","fields","out","name","entries","byteLength","Buffer","JSON","stringify","mapPluginConnectionRecord","sync","answers","meta","summary","record","request","pages","items","declared","item","maxItems","push","kind","over","last","drop","min","max","ceil","splice","pluginConnectionSyncRequestsRead","read","Set","values","add","requests","filter","has","id"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ export declare function recordRoute(request: Request): Promise<Response>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
17
+ import { isListingId } from "./config.js";
18
+ import { connectionRecordRef } from "./connection-store.js";
19
+ import { installedConnection } from "./installed-connection.js";
20
+ import { routeError, routeJson, siteConnectionsGate } from "./routes.js";
21
+ import { toSiteRecord } from "./site-records.js";
22
+ /*
23
+ * THE RECORD, FOR THE EDITOR (AGL-4195).
24
+ *
25
+ * A published page hands a signed element its connection's record with the
26
+ * page (the enricher in `site-records.ts`). The Besigner renders the same
27
+ * element in the console's browser, where there is no page render to ride:
28
+ * this route answers the same record, so the canvas can show what the
29
+ * published page will.
30
+ *
31
+ * GET /api/plugin-connections/record?hostId=&listingId=&connectionId=
32
+ *
33
+ * 200 { record } the record, held to the pinned version's declaration
34
+ * exactly as the page holds it, or `null` before the
35
+ * first sync and on a plan without connections
36
+ * 400 a parameter missing or malformed
37
+ * 401/403/404 the site gate (`siteConnectionsGate`): signed in,
38
+ * verified, "Manage a site's plugin connections" on the site
39
+ * 404 the site runs no plugin that declares the connection
40
+ *
41
+ * The record collection is server-only; this reads it with the Admin SDK
42
+ * for a member holding the site key, which is the same key that connects it.
43
+ */ const methodNotAllowed = ()=>new Response(JSON.stringify({
44
+ error: 'Method not allowed'
45
+ }), {
46
+ status: 405,
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ Allow: 'GET',
50
+ 'Cache-Control': 'no-store'
51
+ }
52
+ });
53
+ const firestore = ()=>firebaseAdmin.app().firestore();
54
+ export async function recordRoute(request) {
55
+ var _ref;
56
+ if (request.method !== 'GET') return methodNotAllowed();
57
+ const params = Object.fromEntries(new URL(request.url).searchParams.entries());
58
+ const actor = await siteConnectionsGate(request, params, {
59
+ requirePlan: false
60
+ });
61
+ if (actor instanceof Response) return actor;
62
+ const listingId = params['listingId'];
63
+ const connectionId = params['connectionId'];
64
+ if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {
65
+ return routeError(400, 'Missing listingId or connectionId');
66
+ }
67
+ const found = await installedConnection(firestore(), {
68
+ hostId: actor.hostId,
69
+ orgId: actor.orgId,
70
+ listingId,
71
+ connectionId
72
+ });
73
+ if (!found) return routeError(404, 'This site runs no plugin that declares that connection.');
74
+ if (!actor.planAllows) return routeJson({
75
+ record: null,
76
+ planAllows: false
77
+ });
78
+ const snapshot = await connectionRecordRef(firestore(), actor.hostId, listingId, connectionId).get();
79
+ const data = snapshot.data();
80
+ const held = data && data['listingId'] === listingId && data['connectionId'] === connectionId ? toSiteRecord(data, found.connection) : null;
81
+ const record = held ? {
82
+ kind: 'record',
83
+ listingId,
84
+ connectionId,
85
+ version: String((_ref = data == null ? void 0 : data['version']) != null ? _ref : ''),
86
+ syncedAtMs: held.syncedAtMs,
87
+ fields: held.fields,
88
+ items: held.items
89
+ } : null;
90
+ return routeJson({
91
+ record,
92
+ planAllows: true
93
+ });
94
+ }
95
+
96
+ //# sourceMappingURL=record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport type { PluginConnectionRecordDoc } from '../../model/plugin-connections'\nimport { isListingId } from './config'\nimport { connectionRecordRef } from './connection-store'\nimport { installedConnection } from './installed-connection'\nimport { routeError, routeJson, siteConnectionsGate } from './routes'\nimport { toSiteRecord } from './site-records'\n\n/*\n * THE RECORD, FOR THE EDITOR (AGL-4195).\n *\n * A published page hands a signed element its connection's record with the\n * page (the enricher in `site-records.ts`). The Besigner renders the same\n * element in the console's browser, where there is no page render to ride:\n * this route answers the same record, so the canvas can show what the\n * published page will.\n *\n * GET /api/plugin-connections/record?hostId=&listingId=&connectionId=\n *\n * 200 { record } the record, held to the pinned version's declaration\n * exactly as the page holds it, or `null` before the\n * first sync and on a plan without connections\n * 400 a parameter missing or malformed\n * 401/403/404 the site gate (`siteConnectionsGate`): signed in,\n * verified, \"Manage a site's plugin connections\" on the site\n * 404 the site runs no plugin that declares the connection\n *\n * The record collection is server-only; this reads it with the Admin SDK\n * for a member holding the site key, which is the same key that connects it.\n */\n\nconst methodNotAllowed = () =>\n new Response(JSON.stringify({ error: 'Method not allowed' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json', Allow: 'GET', 'Cache-Control': 'no-store' },\n })\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\nexport async function recordRoute(request: Request): Promise<Response> {\n if (request.method !== 'GET') return methodNotAllowed()\n const params: Record<string, unknown> = Object.fromEntries(new URL(request.url).searchParams.entries())\n const actor = await siteConnectionsGate(request, params, { requirePlan: false })\n if (actor instanceof Response) return actor\n const listingId = params['listingId']\n const connectionId = params['connectionId']\n if (!isListingId(listingId) || typeof connectionId !== 'string' || !connectionId) {\n return routeError(400, 'Missing listingId or connectionId')\n }\n const found = await installedConnection(firestore(), { hostId: actor.hostId, orgId: actor.orgId, listingId, connectionId })\n if (!found) return routeError(404, 'This site runs no plugin that declares that connection.')\n if (!actor.planAllows) return routeJson({ record: null, planAllows: false })\n const snapshot = await connectionRecordRef(firestore(), actor.hostId, listingId, connectionId).get()\n const data = snapshot.data() as Record<string, unknown> | undefined\n const held = data && data['listingId'] === listingId && data['connectionId'] === connectionId ? toSiteRecord(data, found.connection) : null\n const record: PluginConnectionRecordDoc | null = held\n ? {\n kind: 'record',\n listingId,\n connectionId,\n version: String(data?.['version'] ?? ''),\n syncedAtMs: held.syncedAtMs,\n fields: held.fields,\n items: held.items,\n }\n : null\n return routeJson({ record, planAllows: true })\n}\n"],"names":["firebaseAdmin","isListingId","connectionRecordRef","installedConnection","routeError","routeJson","siteConnectionsGate","toSiteRecord","methodNotAllowed","Response","JSON","stringify","error","status","headers","Allow","firestore","app","recordRoute","request","method","params","Object","fromEntries","URL","url","searchParams","entries","actor","requirePlan","listingId","connectionId","found","hostId","orgId","planAllows","record","snapshot","get","data","held","connection","kind","version","String","syncedAtMs","fields","items"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,2BAA0B;AAExD,SAASC,WAAW,QAAQ,cAAU;AACtC,SAASC,mBAAmB,QAAQ,wBAAoB;AACxD,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SAASC,UAAU,EAAEC,SAAS,EAAEC,mBAAmB,QAAQ,cAAU;AACrE,SAASC,YAAY,QAAQ,oBAAgB;AAE7C;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,MAAMC,mBAAmB,IACvB,IAAIC,SAASC,KAAKC,SAAS,CAAC;QAAEC,OAAO;IAAqB,IAAI;QAC5DC,QAAQ;QACRC,SAAS;YAAE,gBAAgB;YAAoBC,OAAO;YAAO,iBAAiB;QAAW;IAC3F;AAEF,MAAMC,YAAY,IAAmChB,cAAciB,GAAG,GAAGD,SAAS;AAElF,OAAO,eAAeE,YAAYC,OAAgB;;IAChD,IAAIA,QAAQC,MAAM,KAAK,OAAO,OAAOZ;IACrC,MAAMa,SAAkCC,OAAOC,WAAW,CAAC,IAAIC,IAAIL,QAAQM,GAAG,EAAEC,YAAY,CAACC,OAAO;IACpG,MAAMC,QAAQ,MAAMtB,oBAAoBa,SAASE,QAAQ;QAAEQ,aAAa;IAAM;IAC9E,IAAID,iBAAiBnB,UAAU,OAAOmB;IACtC,MAAME,YAAYT,MAAM,CAAC,YAAY;IACrC,MAAMU,eAAeV,MAAM,CAAC,eAAe;IAC3C,IAAI,CAACpB,YAAY6B,cAAc,OAAOC,iBAAiB,YAAY,CAACA,cAAc;QAChF,OAAO3B,WAAW,KAAK;IACzB;IACA,MAAM4B,QAAQ,MAAM7B,oBAAoBa,aAAa;QAAEiB,QAAQL,MAAMK,MAAM;QAAEC,OAAON,MAAMM,KAAK;QAAEJ;QAAWC;IAAa;IACzH,IAAI,CAACC,OAAO,OAAO5B,WAAW,KAAK;IACnC,IAAI,CAACwB,MAAMO,UAAU,EAAE,OAAO9B,UAAU;QAAE+B,QAAQ;QAAMD,YAAY;IAAM;IAC1E,MAAME,WAAW,MAAMnC,oBAAoBc,aAAaY,MAAMK,MAAM,EAAEH,WAAWC,cAAcO,GAAG;IAClG,MAAMC,OAAOF,SAASE,IAAI;IAC1B,MAAMC,OAAOD,QAAQA,IAAI,CAAC,YAAY,KAAKT,aAAaS,IAAI,CAAC,eAAe,KAAKR,eAAexB,aAAagC,MAAMP,MAAMS,UAAU,IAAI;IACvI,MAAML,SAA2CI,OAC7C;QACEE,MAAM;QACNZ;QACAC;QACAY,SAASC,eAAOL,wBAAAA,IAAM,CAAC,UAAU,mBAAI;QACrCM,YAAYL,KAAKK,UAAU;QAC3BC,QAAQN,KAAKM,MAAM;QACnBC,OAAOP,KAAKO,KAAK;IACnB,IACA;IACJ,OAAO1C,UAAU;QAAE+B;QAAQD,YAAY;IAAK;AAC9C"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginConnectionDeclaration } from '@aglyn/aglyn/app-utils/plugin-connections';
18
+ import { type PluginConnectionRecord } from './connection-store';
19
+ /**
20
+ * Revokes a record's grant at the provider, best effort: a grant that cannot
21
+ * be revoked is still forgotten here, and the owner can remove the app on the
22
+ * provider's side. Never throws. Shared with the erasers.
23
+ *
24
+ * The revocation endpoint is the one the site's pinned version declares. A
25
+ * caller that has already lost the pin — an uninstall, a version that no
26
+ * longer declares the connection (AGL-4194) — passes the declaration it read
27
+ * before the pin moved.
28
+ */
29
+ export declare function revokeRecord(record: PluginConnectionRecord, orgId: string | null, declaration?: PluginConnectionDeclaration | null): Promise<boolean>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
17
+ import { readPluginConnectionsKeyring } from "./config.js";
18
+ import { openToken } from "./connection-store.js";
19
+ import { openCredentials } from "./credentials-store.js";
20
+ import { installedConnection } from "./installed-connection.js";
21
+ import { revokePluginConnectionToken } from "./provider-http.js";
22
+ /**
23
+ * Revokes a record's grant at the provider, best effort: a grant that cannot
24
+ * be revoked is still forgotten here, and the owner can remove the app on the
25
+ * provider's side. Never throws. Shared with the erasers.
26
+ *
27
+ * The revocation endpoint is the one the site's pinned version declares. A
28
+ * caller that has already lost the pin — an uninstall, a version that no
29
+ * longer declares the connection (AGL-4194) — passes the declaration it read
30
+ * before the pin moved.
31
+ */ export async function revokeRecord(record, orgId, declaration) {
32
+ const keyring = readPluginConnectionsKeyring();
33
+ if (!keyring) return false;
34
+ try {
35
+ var _this;
36
+ const connection = declaration != null ? declaration : (_this = await installedConnection(firebaseAdmin.app().firestore(), {
37
+ hostId: record.hostId,
38
+ orgId,
39
+ listingId: record.listingId,
40
+ connectionId: record.connectionId
41
+ })) == null ? void 0 : _this.connection;
42
+ if (!(connection == null ? void 0 : connection.revocationUrl)) return false;
43
+ const client = await openCredentials(firebaseAdmin.app().firestore(), keyring, record.listingId, record.connectionId).catch(()=>null);
44
+ const token = record.sealedRefreshToken ? openToken(keyring, record, 'refresh').token : openToken(keyring, record, 'access').token;
45
+ return await revokePluginConnectionToken(connection, client, token);
46
+ } catch (error) {
47
+ console.warn('plugin-connections: revoke failed', error instanceof Error ? error.message : error);
48
+ return false;
49
+ }
50
+ }
51
+
52
+ //# sourceMappingURL=revoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/revoke.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginConnectionDeclaration } from '@aglyn/aglyn/app-utils/plugin-connections'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { readPluginConnectionsKeyring } from './config'\nimport { openToken, type PluginConnectionRecord } from './connection-store'\nimport { openCredentials } from './credentials-store'\nimport { installedConnection } from './installed-connection'\nimport { revokePluginConnectionToken } from './provider-http'\n\n/**\n * Revokes a record's grant at the provider, best effort: a grant that cannot\n * be revoked is still forgotten here, and the owner can remove the app on the\n * provider's side. Never throws. Shared with the erasers.\n *\n * The revocation endpoint is the one the site's pinned version declares. A\n * caller that has already lost the pin — an uninstall, a version that no\n * longer declares the connection (AGL-4194) — passes the declaration it read\n * before the pin moved.\n */\nexport async function revokeRecord(\n record: PluginConnectionRecord,\n orgId: string | null,\n declaration?: PluginConnectionDeclaration | null,\n): Promise<boolean> {\n const keyring = readPluginConnectionsKeyring()\n if (!keyring) return false\n try {\n const connection =\n declaration ??\n (\n await installedConnection(firebaseAdmin.app().firestore(), {\n hostId: record.hostId,\n orgId,\n listingId: record.listingId,\n connectionId: record.connectionId,\n })\n )?.connection\n if (!connection?.revocationUrl) return false\n const client = await openCredentials(firebaseAdmin.app().firestore(), keyring, record.listingId, record.connectionId).catch(() => null)\n const token = record.sealedRefreshToken\n ? openToken(keyring, record, 'refresh').token\n : openToken(keyring, record, 'access').token\n return await revokePluginConnectionToken(connection, client, token)\n } catch (error) {\n console.warn('plugin-connections: revoke failed', error instanceof Error ? error.message : error)\n return false\n }\n}\n\n"],"names":["firebaseAdmin","readPluginConnectionsKeyring","openToken","openCredentials","installedConnection","revokePluginConnectionToken","revokeRecord","record","orgId","declaration","keyring","connection","app","firestore","hostId","listingId","connectionId","revocationUrl","client","catch","token","sealedRefreshToken","error","console","warn","Error","message"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SAASC,4BAA4B,QAAQ,cAAU;AACvD,SAASC,SAAS,QAAqC,wBAAoB;AAC3E,SAASC,eAAe,QAAQ,yBAAqB;AACrD,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SAASC,2BAA2B,QAAQ,qBAAiB;AAE7D;;;;;;;;;CASC,GACD,OAAO,eAAeC,aACpBC,MAA8B,EAC9BC,KAAoB,EACpBC,WAAgD;IAEhD,MAAMC,UAAUT;IAChB,IAAI,CAACS,SAAS,OAAO;IACrB,IAAI;YAIE;QAHJ,MAAMC,aACJF,sBAAAA,eAEE,QAAA,MAAML,oBAAoBJ,cAAcY,GAAG,GAAGC,SAAS,IAAI;YACzDC,QAAQP,OAAOO,MAAM;YACrBN;YACAO,WAAWR,OAAOQ,SAAS;YAC3BC,cAAcT,OAAOS,YAAY;QACnC,uBANF,AACE,MAMCL,UAAU;QACf,IAAI,EAACA,8BAAAA,WAAYM,aAAa,GAAE,OAAO;QACvC,MAAMC,SAAS,MAAMf,gBAAgBH,cAAcY,GAAG,GAAGC,SAAS,IAAIH,SAASH,OAAOQ,SAAS,EAAER,OAAOS,YAAY,EAAEG,KAAK,CAAC,IAAM;QAClI,MAAMC,QAAQb,OAAOc,kBAAkB,GACnCnB,UAAUQ,SAASH,QAAQ,WAAWa,KAAK,GAC3ClB,UAAUQ,SAASH,QAAQ,UAAUa,KAAK;QAC9C,OAAO,MAAMf,4BAA4BM,YAAYO,QAAQE;IAC/D,EAAE,OAAOE,OAAO;QACdC,QAAQC,IAAI,CAAC,qCAAqCF,iBAAiBG,QAAQH,MAAMI,OAAO,GAAGJ;QAC3F,OAAO;IACT;AACF"}