@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15

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 (253) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +102 -52
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/capability-stewardship.md +1 -1
  10. package/docs/blocks/claims.md +2 -2
  11. package/docs/blocks/conformance-admission.md +1 -1
  12. package/docs/blocks/learning-harnesses.md +1 -1
  13. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  14. package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
  15. package/docs/guides/co-authoring.md +113 -0
  16. package/docs/{distributions.md → guides/distributions.md} +13 -8
  17. package/docs/guides/extensions.md +189 -0
  18. package/docs/guides/graph-refresh.md +152 -0
  19. package/docs/guides/graph-workspace.md +64 -0
  20. package/docs/{ingestion.md → guides/ingestion.md} +16 -15
  21. package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
  22. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  23. package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
  24. package/docs/{learning.md → guides/learning.md} +12 -5
  25. package/docs/{obsidian.md → guides/obsidian.md} +370 -23
  26. package/docs/guides/ontology-guide.md +111 -0
  27. package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
  28. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  29. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  30. package/docs/{upgrade.md → guides/upgrade.md} +197 -11
  31. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  32. package/docs/knowledge-health/facilitator.md +15 -4
  33. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  34. package/docs/knowledge-health/quickstart.md +97 -75
  35. package/docs/knowledge-health/readme.md +13 -5
  36. package/docs/maintainers/describing-the-atelier.md +173 -0
  37. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
  38. package/docs/{announcements.md → reference/announcements.md} +1 -1
  39. package/docs/{architecture.md → reference/architecture.md} +31 -5
  40. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  41. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  42. package/docs/{coordination.md → reference/coordination.md} +1 -1
  43. package/docs/{decisions.md → reference/decisions.md} +3 -3
  44. package/docs/{design.md → reference/design.md} +1 -1
  45. package/docs/reference/edge-policy.md +151 -0
  46. package/docs/reference/graph-view-structured.md +119 -0
  47. package/docs/reference/graph-view.md +139 -0
  48. package/docs/reference/graph-workspace.md +37 -0
  49. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  50. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  51. package/docs/{local-services.md → reference/local-services.md} +43 -4
  52. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
  53. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
  54. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  55. package/docs/reference/portability.md +92 -0
  56. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  57. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  58. package/docs/{project-options.md → reference/project-options.md} +17 -0
  59. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
  60. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  61. package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
  62. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  63. package/docs/{trackables.md → reference/trackables.md} +2 -2
  64. package/docs/start/independent-start.md +89 -0
  65. package/docs/{install.md → start/install.md} +9 -9
  66. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  67. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  68. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  69. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  70. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  71. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  72. package/fixtures/knowledge-health/workshop/README.md +1 -1
  73. package/fixtures/templates/local-library.v1.json +3 -1
  74. package/package.json +22 -8
  75. package/plugins/obsidian/main.js +57 -8
  76. package/plugins/obsidian/manifest.json +1 -1
  77. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  78. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  79. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  80. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  81. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  82. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  83. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  84. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  85. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  86. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  87. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  88. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  89. package/src/architecture/catalog.mjs +2 -2
  90. package/src/attestation/sign.mjs +1 -1
  91. package/src/boundary/content-rules.mjs +534 -68
  92. package/src/boundary/policy.mjs +187 -60
  93. package/src/cli/run.mjs +26 -9
  94. package/src/commands/distribution.mjs +1 -1
  95. package/src/commands/extension.mjs +27 -0
  96. package/src/commands/ingest.mjs +1 -1
  97. package/src/commands/learn.mjs +1 -1
  98. package/src/commands/obsidian.mjs +49 -21
  99. package/src/commands/review.mjs +3 -0
  100. package/src/edge-policy/contract.mjs +69 -0
  101. package/src/edge-policy/policy.mjs +142 -0
  102. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  103. package/src/extension-packs/loader.mjs +4 -3
  104. package/src/extensions/admission.mjs +83 -0
  105. package/src/extensions/client.d.mts +23 -0
  106. package/src/extensions/client.mjs +99 -0
  107. package/src/extensions/contracts.d.mts +29 -0
  108. package/src/extensions/contracts.mjs +163 -0
  109. package/src/extensions/convex.d.mts +24 -0
  110. package/src/extensions/convex.mjs +189 -0
  111. package/src/extensions/delegation.mjs +54 -0
  112. package/src/extensions/drizzle.d.mts +11 -0
  113. package/src/extensions/drizzle.mjs +33 -0
  114. package/src/extensions/postgres.d.mts +7 -0
  115. package/src/extensions/postgres.mjs +66 -0
  116. package/src/extensions/prisma.d.mts +13 -0
  117. package/src/extensions/prisma.mjs +63 -0
  118. package/src/extensions/project.mjs +432 -0
  119. package/src/extensions/scaffold-convex.mjs +130 -0
  120. package/src/extensions/scaffold-sql.mjs +144 -0
  121. package/src/extensions/server.d.mts +20 -0
  122. package/src/extensions/server.mjs +102 -0
  123. package/src/extensions/sql-scope.mjs +22 -0
  124. package/src/graph/graph.mjs +13 -5
  125. package/src/graph/knowledge-graph.mjs +20 -2
  126. package/src/graph/repo-contribution.mjs +405 -0
  127. package/src/graph/repo-contribution.types.mjs +32 -0
  128. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  129. package/src/graph-access/edge-record-adapter.mjs +73 -0
  130. package/src/graph-access/index.mjs +253 -0
  131. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  132. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  133. package/src/graph-access/source-adapter.mjs +81 -0
  134. package/src/graph-refresh/compare.mjs +110 -0
  135. package/src/graph-refresh/controller.mjs +324 -0
  136. package/src/graph-refresh/index.mjs +3 -0
  137. package/src/graph-refresh/local.mjs +78 -0
  138. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  139. package/src/graph-view/babylon/index.mjs +2 -0
  140. package/src/graph-view/babylon/navigator.mjs +312 -0
  141. package/src/graph-view/babylon/provenance.mjs +36 -0
  142. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  143. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  144. package/src/graph-view/babylon/web-host.mjs +188 -0
  145. package/src/graph-view/cli.mjs +3 -0
  146. package/src/graph-view/command.mjs +34 -0
  147. package/src/graph-view/contracts.mjs +79 -0
  148. package/src/graph-view/errors.mjs +5 -0
  149. package/src/graph-view/index.mjs +4 -0
  150. package/src/graph-view/intents.mjs +30 -0
  151. package/src/graph-view/query.mjs +213 -0
  152. package/src/graph-view/refs.mjs +2 -0
  153. package/src/graph-view/structured/browser.mjs +262 -0
  154. package/src/graph-view/structured/styles.mjs +8 -0
  155. package/src/graph-workspace/authoring.mjs +185 -0
  156. package/src/graph-workspace/browser/constellation.mjs +369 -0
  157. package/src/graph-workspace/browser/dom.mjs +23 -0
  158. package/src/graph-workspace/browser/index.mjs +6 -0
  159. package/src/graph-workspace/browser/review.mjs +55 -0
  160. package/src/graph-workspace/browser/styles.mjs +11 -0
  161. package/src/graph-workspace/browser/workbench.mjs +351 -0
  162. package/src/graph-workspace/browser/workspace.mjs +431 -0
  163. package/src/graph-workspace/disclosure.mjs +39 -0
  164. package/src/graph-workspace/geometry.mjs +78 -0
  165. package/src/graph-workspace/index.mjs +11 -0
  166. package/src/graph-workspace/labels.mjs +50 -0
  167. package/src/graph-workspace/model.mjs +109 -0
  168. package/src/graph-workspace/recipes.mjs +88 -0
  169. package/src/graph-workspace/scale.mjs +95 -0
  170. package/src/graph-workspace/theme.mjs +156 -0
  171. package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
  172. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  173. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  174. package/src/knowledge/ontology-guide.mjs +152 -0
  175. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  176. package/src/knowledge-health/rule-identity.mjs +14 -9
  177. package/src/knowledge-health/workshop.mjs +22 -2
  178. package/src/personal-workspace/index.mjs +1 -1
  179. package/src/personal-workspace/selection.mjs +34 -3
  180. package/src/project/config.mjs +1 -1
  181. package/src/project/path-match.mjs +2 -1
  182. package/src/project/providers/github.mjs +24 -0
  183. package/src/project/repo-identity.mjs +3 -22
  184. package/src/projection/obsidian/contracts.mjs +1 -1
  185. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  186. package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
  187. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  188. package/src/projection/obsidian/personal-workspace.mjs +84 -5
  189. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  190. package/src/projection/obsidian/proposals/adapter.mjs +46 -23
  191. package/src/projection/obsidian/proposals/observation.mjs +40 -2
  192. package/src/projection/obsidian/publication/publisher.mjs +96 -8
  193. package/src/projection/project.mjs +21 -16
  194. package/src/readiness/readiness.mjs +20 -11
  195. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  196. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  197. package/src/readiness-protocols/evidence.mjs +2 -1
  198. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  199. package/src/readiness-protocols/runtime.mjs +43 -6
  200. package/src/runtime/obsidian/engine.mjs +387 -76
  201. package/src/runtime/obsidian/lifecycle.mjs +20 -15
  202. package/src/runtime/obsidian/login-item.mjs +6 -3
  203. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  204. package/src/runtime/obsidian/observation.mjs +36 -4
  205. package/src/runtime/obsidian/opening.mjs +4 -3
  206. package/src/runtime/obsidian/personal-selection.mjs +187 -0
  207. package/src/runtime/obsidian/pipeline.mjs +115 -6
  208. package/src/runtime/obsidian/private-lock.mjs +50 -6
  209. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  210. package/src/runtime/obsidian/service-main.mjs +68 -16
  211. package/src/runtime/obsidian/service.mjs +80 -6
  212. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  213. package/src/runtime/obsidian/view-permission.mjs +335 -0
  214. package/src/ui/control-system.mjs +201 -0
  215. package/src/ui/html-primitives.mjs +3 -0
  216. package/src/upgrade/closure-diagnostics.mjs +135 -0
  217. package/src/upgrade/closure.mjs +649 -0
  218. package/src/upgrade/upgrade.mjs +42 -11
  219. package/templates/distribution-workspace/README.md +2 -2
  220. package/templates/external-project-workspace/atelier.project.json +3 -0
  221. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  222. package/templates/knowledge-workspace/README.md +2 -2
  223. package/docs/integration-and-review-plan.md +0 -409
  224. package/docs/integration-contract-decisions.md +0 -44
  225. package/docs/knowledge-health/devday/facilitator.md +0 -16
  226. package/docs/knowledge-health/devday/public-default.md +0 -107
  227. package/docs/knowledge-health/devday/quickstart.md +0 -18
  228. package/docs/knowledge-health/devday/support.md +0 -22
  229. package/docs/knowledge-health/exercise.md +0 -17
  230. package/docs/knowledge-health/support.md +0 -34
  231. package/docs/presentation-review-disposition.md +0 -47
  232. package/docs/progressive-ingestion-acceptance.md +0 -89
  233. package/docs/progressive-ingestion.md +0 -235
  234. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  235. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  236. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  237. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  238. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  239. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  240. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  241. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  242. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  243. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  244. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  245. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  246. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  247. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  248. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  249. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  250. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  251. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  252. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  253. /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
@@ -0,0 +1,99 @@
1
+ import { canonicalJson, exact, ExtensionError, parseInstallationBinding, parseManifest, parseOperationReceipt, parseReleaseBundle, text } from './contracts.mjs'
2
+
3
+ const opaque = value => { if (!/^[A-Za-z0-9_-]{16,128}$/.test(text(value))) throw new ExtensionError('invalid-input'); return value }
4
+ const actionIdValue = value => { if (!/^[a-z0-9][a-z0-9._-]{2,127}$/.test(text(value)) || /^(?:lifecycle|installation|extension)\./.test(value)) throw new ExtensionError('invalid-input'); return value }
5
+ const codes = new Set(['invalid-input', 'unauthorized', 'forbidden', 'stale-installation', 'disabled', 'idempotency-conflict', 'operation-unknown', 'release-conflict', 'unavailable'])
6
+ const registration = (value, releaseDigest) => {
7
+ const result=exact(JSON.parse(canonicalJson(value)),['releaseId','releaseDigest'])
8
+ if(result.releaseDigest!==releaseDigest)throw new ExtensionError('unavailable',503)
9
+ return {releaseId:opaque(result.releaseId),releaseDigest:result.releaseDigest}
10
+ }
11
+ // The caller supplies an authenticated host fetch; destinations and credentials are host-owned.
12
+ export function createHttpHostTransport(hostFetch) {
13
+ if (typeof hostFetch !== 'function') throw new ExtensionError('invalid-input')
14
+ const base = '/api/root-runtime/extensions'
15
+ const segment = value => encodeURIComponent(opaque(value))
16
+ async function send(path, body) {
17
+ const response = await hostFetch(`${base}${path}`, { method: body === undefined ? 'GET' : 'POST',
18
+ credentials: 'same-origin', cache: 'no-store', redirect: 'error',
19
+ ...(body === undefined ? {} : { headers: { 'content-type': 'application/json' }, body: canonicalJson(body) }) })
20
+ let value
21
+ const reader = response.body?.getReader()
22
+ if (!reader) throw new ExtensionError('unavailable', 503)
23
+ try {
24
+ const chunks = []; let size = 0
25
+ for (;;) {
26
+ const part = await reader.read()
27
+ if (part.done) break
28
+ size += part.value.byteLength
29
+ if (size > 131072) throw new ExtensionError('unavailable', 503)
30
+ chunks.push(part.value)
31
+ }
32
+ const bytes = new Uint8Array(size); let offset = 0
33
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }
34
+ value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
35
+ } catch { throw new ExtensionError('unavailable', 503) } finally { await reader.cancel().catch(() => {}) }
36
+ if (!response.ok) throw new ExtensionError(codes.has(value?.code) ? value.code : 'unavailable', response.status)
37
+ return value
38
+ }
39
+ const installation = id => `/installations/${segment(id)}`
40
+ return Object.freeze({
41
+ registerRelease: async input => {
42
+ const bundle=parseReleaseBundle(input), expectedReleaseDigest=bundle.manifest.releaseDigest
43
+ return registration(await send('/releases',bundle),expectedReleaseDigest)
44
+ },
45
+ getInstallation: async id => parseInstallationBinding(await send(installation(id))),
46
+ invoke: async request => {
47
+ const { installationId, actionId, ...body } = request
48
+ return parseOperationReceipt(await send(`${installation(installationId)}/actions/${encodeURIComponent(actionIdValue(actionId))}`, body))
49
+ },
50
+ lookupOperation: async (id, idempotencyKey, actionId) => {
51
+ const value = await send(`${installation(id)}/lookup`, { idempotencyKey, actionId })
52
+ return value === null ? null : parseOperationReceipt(value)
53
+ },
54
+ recoverOperation: async ({ installationId, ...binding }, idempotencyKey, actionId) =>
55
+ parseOperationReceipt(await send(`${installation(installationId)}/recover`, { ...binding, idempotencyKey, actionId })),
56
+ recordResult: async operationId => {
57
+ const value = exact(await send(`/operations/${segment(operationId)}/result`, {}), ['recorded'])
58
+ if (value.recorded !== true) throw new ExtensionError('unavailable', 503)
59
+ },
60
+ configure: async (manifest, connectionRef, environment) => parseInstallationBinding(await send('/installations', { manifest, connectionRef, environment })),
61
+ transition: async ({ installationId, ...binding }, transition) => parseInstallationBinding(await send(`${installation(installationId)}/lifecycle`, { ...binding, transition })),
62
+ upgrade: async ({ installationId, ...binding }, manifest) => parseInstallationBinding(await send(`${installation(installationId)}/upgrade`, { ...binding, manifest })),
63
+ })
64
+ }
65
+ export function createExtensionClient(transport) {
66
+ return Object.freeze({
67
+ getInstallation: async id => parseInstallationBinding(await transport.getInstallation(opaque(id))),
68
+ invoke: async (binding, actionId, input, idempotencyKey) => parseOperationReceipt(await transport.invoke({
69
+ ...parseInstallationBinding(binding), actionId: actionIdValue(actionId), idempotencyKey: text(idempotencyKey), input: JSON.parse(canonicalJson(input)),
70
+ })),
71
+ lookupOperation: async (id, key, action) => {
72
+ const result = await transport.lookupOperation(opaque(id), text(key), actionIdValue(action))
73
+ return result === null ? null : parseOperationReceipt(result)
74
+ },
75
+ recoverOperation: async (binding, key, action) => parseOperationReceipt(await transport.recoverOperation(parseInstallationBinding(binding), text(key), actionIdValue(action))),
76
+ recordResult: async id => {
77
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(text(id))) throw new ExtensionError('invalid-input')
78
+ await transport.recordResult(id)
79
+ },
80
+ })
81
+ }
82
+ export function createExtensionAdminClient(transport) {
83
+ return Object.freeze({
84
+ registerRelease: async value => {
85
+ if(typeof transport.registerRelease!=='function')throw new ExtensionError('unavailable',503)
86
+ const bundle=parseReleaseBundle(value), expectedReleaseDigest=bundle.manifest.releaseDigest
87
+ return registration(await transport.registerRelease(bundle),expectedReleaseDigest)
88
+ },
89
+ configure: async (manifest, connectionRef, environment = 'development') => {
90
+ if (!['development', 'staging', 'production'].includes(environment)) throw new ExtensionError('invalid-input')
91
+ return parseInstallationBinding(await transport.configure(parseManifest(manifest), text(connectionRef), environment))
92
+ },
93
+ transition: async (binding, transition) => {
94
+ if (!['verify', 'enable', 'disable', 'remove'].includes(transition)) throw new ExtensionError('invalid-input')
95
+ return parseInstallationBinding(await transport.transition(parseInstallationBinding(binding), transition))
96
+ },
97
+ upgrade: async (binding, manifest) => parseInstallationBinding(await transport.upgrade(parseInstallationBinding(binding), parseManifest(manifest))),
98
+ })
99
+ }
@@ -0,0 +1,29 @@
1
+ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }
2
+ export interface InstallationBinding { installationId: string; generation: string; releaseDigest: string }
3
+ export interface Invocation extends InstallationBinding { actionId: string; idempotencyKey: string; input: Json }
4
+ export interface OperationReceipt { operationId: string; state: 'pending' | 'admitted' | 'succeeded' | 'unknown' | 'failed'; result?: Json; resultDigest?: string }
5
+ export interface ReservedExtensionFields { ext?: Record<string, never> }
6
+ export interface ActionManifest extends ReservedExtensionFields { id: string; effect: 'read' | 'write'; scope: 'member' | 'tenant'; inputSchemaRef: string; outputSchemaRef: string; timeoutMs: number }
7
+ export interface ExtensionManifest extends ReservedExtensionFields {
8
+ contractVersion?: string;
9
+ schema: 'mnstry.extension-manifest/v1'; extensionId: string; releaseVersion: string; releaseDigest: string; protocol: 'mnstry.extension/v1';
10
+ compatibility: ReservedExtensionFields & { sdkMajor: 1; rootContracts: string[]; hosts: ('web' | 'native')[] };
11
+ deployment: ReservedExtensionFields & { profile: 'postgres-sql' | 'drizzle-db-first' | 'drizzle-code-first' | 'prisma-7-postgres' | 'prisma-8-postgres-experimental' | 'convex-native'; bundleDigest: string; schemaDigest: string }; actions: ActionManifest[];
12
+ recovery?: ReservedExtensionFields & { lookupActionId: string }; permissions: ReservedExtensionFields & { rootReads: string[]; rootWrites: string[]; egress: string[] };
13
+ surfaces: (ReservedExtensionFields & { id: string; compositionDigest: string; actionIds: string[] })[];
14
+ data: ReservedExtensionFields & { classes: string[]; retention: 'until-user-delete'; exportVersion: 1; schemaVersion: string; readableSchemaVersions: string[] }
15
+ }
16
+ export class ExtensionError extends Error { readonly code: string; readonly status: number; constructor(code: string, status?: number) }
17
+ export function canonicalJson(value: unknown): string
18
+ export function text(value: unknown, limit?: number): string
19
+ export function exact(value: unknown, required: string[], optional?: string[]): Record<string, unknown>
20
+ export function parseManifest(value: unknown): ExtensionManifest
21
+ export function parseInstallationBinding(value: unknown): InstallationBinding
22
+ export function parseOperationReceipt(value: unknown): OperationReceipt
23
+
24
+ export interface ProviderOperationProof extends InstallationBinding { schema: 'mnstry.extension-provider-operation/v1'; operationId: string; actionId: string; payloadDigest: string; resultDigest: string; result: Json }
25
+ export function parseProviderOperationProof(value: unknown): ProviderOperationProof
26
+
27
+ export interface ExtensionReleaseBundle { manifest: ExtensionManifest; schemaFiles: Record<string, string>; migrationDigests: Record<string, string> }
28
+ export interface ExtensionReleaseRegistration { releaseId: string; releaseDigest: string }
29
+ export function parseReleaseBundle(value: unknown): ExtensionReleaseBundle
@@ -0,0 +1,163 @@
1
+ // Portable wire contracts. Host authentication and persistence stay with the consumer.
2
+ export class ExtensionError extends Error {
3
+ constructor(code, status = 400) { super(code); this.name = 'ExtensionError'; this.code = code; this.status = status }
4
+ }
5
+ const reject = () => { throw new ExtensionError('invalid-input') }
6
+ export function canonicalJson(value) {
7
+ const seen = new Set()
8
+ function visit(item, depth) {
9
+ if (depth > 16) return reject()
10
+ if (item === null || typeof item === 'boolean') return JSON.stringify(item)
11
+ if (typeof item === 'number') return Number.isFinite(item) ? JSON.stringify(item) : reject()
12
+ if (typeof item === 'string') {
13
+ for (let n = 0; n < item.length; n++) {
14
+ const c = item.charCodeAt(n)
15
+ if (c >= 0xd800 && c <= 0xdbff) {
16
+ const next = item.charCodeAt(++n)
17
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return reject()
18
+ } else if (c >= 0xdc00 && c <= 0xdfff) return reject()
19
+ }
20
+ return JSON.stringify(item)
21
+ }
22
+ if (!item || typeof item !== 'object' || seen.has(item)) return reject()
23
+ seen.add(item)
24
+ let result
25
+ if (Array.isArray(item)) {
26
+ if (Reflect.ownKeys(item).length !== item.length + 1 || Array.from({ length: item.length }, (_, i) => i).some(i => !Object.hasOwn(item, i) || !Object.hasOwn(Object.getOwnPropertyDescriptor(item, String(i)), 'value'))) return reject()
27
+ result = `[${item.map(x => visit(x, depth + 1)).join(',')}]`
28
+ } else {
29
+ if (Object.getPrototypeOf(item) !== Object.prototype || Reflect.ownKeys(item).some(k => typeof k !== 'string')) return reject()
30
+ const keys = Reflect.ownKeys(item).sort()
31
+ if (keys.some(k => !Object.getOwnPropertyDescriptor(item, k).enumerable)) return reject()
32
+ if (keys.some(k => !Object.hasOwn(Object.getOwnPropertyDescriptor(item, k), 'value'))) return reject()
33
+ result = `{${keys.map(k => `${visit(k, depth + 1)}:${visit(item[k], depth + 1)}`).join(',')}}`
34
+ }
35
+ seen.delete(item)
36
+ return result
37
+ }
38
+ const encoded = visit(value, 0)
39
+ if (new TextEncoder().encode(encoded).length > 131072) return reject()
40
+ return encoded
41
+ }
42
+ export function text(value, limit = 128) {
43
+ if (typeof value !== 'string' || !value.length || [...value].length > limit || /[\u0000-\u001f]/u.test(value)) return reject()
44
+ canonicalJson(value)
45
+ return value
46
+ }
47
+ export function exact(value, required, optional = []) {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype ||
49
+ required.some(k => !Object.hasOwn(value, k)) || Object.keys(value).some(k => !required.includes(k) && !optional.includes(k))) return reject()
50
+ return value
51
+ }
52
+ const digest = value => /^[a-f0-9]{64}$/.test(text(value, 64)) ? value : reject()
53
+ const strings = (value, max = 64) => {
54
+ if (!Array.isArray(value) || value.length > max || value.some(x => typeof x !== 'string') || new Set(value).size !== value.length) return reject()
55
+ return value.map(x => text(x, 256))
56
+ }
57
+ const schemaRef = value => {
58
+ text(value, 256)
59
+ if (!/^schemas\/[a-zA-Z0-9._-]+\.json$/.test(value) || value.startsWith('/') || value.split('/').some(x => !x || x === '.' || x === '..')) return reject()
60
+ return value
61
+ }
62
+ function manifestObject(value, required, optional = []) {
63
+ exact(value, required, [...optional, 'ext'])
64
+ if (value.ext !== undefined) exact(value.ext, [])
65
+ }
66
+ export function parseManifest(value) {
67
+ const m = JSON.parse(canonicalJson(value))
68
+ manifestObject(m, ['schema', 'extensionId', 'releaseVersion', 'releaseDigest', 'protocol', 'compatibility', 'deployment', 'actions', 'permissions', 'surfaces', 'data'], ['recovery', 'contractVersion'])
69
+ if (m.contractVersion !== undefined && !/^1\.[0-9]+\.[0-9]+$/.test(text(m.contractVersion))) return reject()
70
+ if (m.schema !== 'mnstry.extension-manifest/v1' || m.protocol !== 'mnstry.extension/v1' ||
71
+ !/^[a-z0-9][a-z0-9._-]{2,127}$/.test(text(m.extensionId)) ||
72
+ !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(text(m.releaseVersion))) return reject()
73
+ const prerelease = m.releaseVersion.split('+')[0].split('-').slice(1).join('-')
74
+ if (prerelease.split('.').some(part => /^0\d+$/.test(part))) return reject()
75
+ digest(m.releaseDigest)
76
+ manifestObject(m.compatibility, ['sdkMajor', 'rootContracts', 'hosts'])
77
+ if (m.compatibility.sdkMajor !== 1 || !m.compatibility.hosts.length || strings(m.compatibility.hosts).some(x => !['web', 'native'].includes(x))) return reject()
78
+ strings(m.compatibility.rootContracts)
79
+ manifestObject(m.deployment, ['profile', 'bundleDigest', 'schemaDigest'])
80
+ if (!['postgres-sql', 'drizzle-db-first', 'drizzle-code-first', 'prisma-7-postgres', 'prisma-8-postgres-experimental', 'convex-native'].includes(text(m.deployment.profile))) return reject()
81
+ digest(m.deployment.bundleDigest); digest(m.deployment.schemaDigest)
82
+ if (!Array.isArray(m.actions) || !m.actions.length || m.actions.length > 64) return reject()
83
+ const actions = new Map()
84
+ for (const action of m.actions) {
85
+ manifestObject(action, ['id', 'effect', 'scope', 'inputSchemaRef', 'outputSchemaRef', 'timeoutMs'])
86
+ text(action.id)
87
+ if (!/^[a-z0-9][a-z0-9._-]{2,127}$/.test(action.id) || actions.has(action.id) || /^(?:lifecycle|installation|extension)\./.test(action.id) ||
88
+ !['read', 'write'].includes(action.effect) || !['member', 'tenant'].includes(action.scope) ||
89
+ !Number.isInteger(action.timeoutMs) || action.timeoutMs < 1 || action.timeoutMs > 30000) return reject()
90
+ schemaRef(action.inputSchemaRef); schemaRef(action.outputSchemaRef); actions.set(action.id, action)
91
+ }
92
+ if (m.recovery !== undefined) {
93
+ manifestObject(m.recovery, ['lookupActionId'])
94
+ const action = actions.get(m.recovery.lookupActionId)
95
+ if (!action || action.effect !== 'read' || action.scope !== 'member') return reject()
96
+ }
97
+ manifestObject(m.permissions, ['rootReads', 'rootWrites', 'egress'])
98
+ for (const key of ['rootReads', 'rootWrites', 'egress']) strings(m.permissions[key])
99
+ if (!Array.isArray(m.surfaces) || m.surfaces.length > 32) return reject()
100
+ const surfaces = new Set()
101
+ for (const surface of m.surfaces) {
102
+ manifestObject(surface, ['id', 'compositionDigest', 'actionIds']); text(surface.id); digest(surface.compositionDigest)
103
+ if (!/^[a-z0-9][a-z0-9._-]{2,127}$/.test(surface.id) || surfaces.has(surface.id) || strings(surface.actionIds).some(id => !actions.has(id))) return reject()
104
+ surfaces.add(surface.id)
105
+ }
106
+ manifestObject(m.data, ['classes', 'retention', 'exportVersion', 'schemaVersion', 'readableSchemaVersions'])
107
+ strings(m.data.classes); text(m.data.schemaVersion)
108
+ if (m.data.retention !== 'until-user-delete' || m.data.exportVersion !== 1 ||
109
+ !strings(m.data.readableSchemaVersions).includes(m.data.schemaVersion)) return reject()
110
+ return m
111
+ }
112
+ export function parseInstallationBinding(value) {
113
+ value = JSON.parse(canonicalJson(value))
114
+ exact(value, ['installationId', 'generation', 'releaseDigest'])
115
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(text(value.installationId)) ||
116
+ !/^[1-9][0-9]{0,18}$/.test(text(value.generation)) || BigInt(value.generation) > 9223372036854775807n) return reject()
117
+ digest(value.releaseDigest)
118
+ return { ...value }
119
+ }
120
+ export function parseOperationReceipt(value) {
121
+ value = JSON.parse(canonicalJson(value))
122
+ exact(value, ['operationId', 'state'], ['result', 'resultDigest'])
123
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(text(value.operationId)) || !['pending', 'admitted', 'succeeded', 'unknown', 'failed'].includes(value.state)) return reject()
124
+ if (value.state === 'succeeded') {
125
+ if (!Object.hasOwn(value, 'result')) return reject()
126
+ digest(value.resultDigest); canonicalJson(value.result)
127
+ } else if (Object.hasOwn(value, 'result') || Object.hasOwn(value, 'resultDigest')) return reject()
128
+ return JSON.parse(canonicalJson(value))
129
+ }
130
+
131
+ export function parseProviderOperationProof(value) {
132
+ const proof = JSON.parse(canonicalJson(value))
133
+ exact(proof, ['schema', 'installationId', 'generation', 'releaseDigest', 'operationId', 'actionId', 'payloadDigest', 'resultDigest', 'result'])
134
+ if (proof.schema !== 'mnstry.extension-provider-operation/v1' || !/^[A-Za-z0-9_-]{16,128}$/.test(text(proof.operationId))) return reject()
135
+ parseInstallationBinding({ installationId: proof.installationId, generation: proof.generation, releaseDigest: proof.releaseDigest })
136
+ text(proof.actionId); digest(proof.payloadDigest); digest(proof.resultDigest)
137
+ return proof
138
+ }
139
+
140
+
141
+ /** Portable registration input. The receiving host independently verifies the
142
+ * release and file hashes; this parser performs no I/O or migration execution. */
143
+ export function parseReleaseBundle(value) {
144
+ const bundle=exact(JSON.parse(canonicalJson(value)),['manifest','schemaFiles','migrationDigests'])
145
+ bundle.manifest=parseManifest(bundle.manifest)
146
+ for(const field of ['schemaFiles','migrationDigests']) {
147
+ const map=bundle[field]
148
+ if(!map||typeof map!=='object'||Array.isArray(map)||Object.getPrototypeOf(map)!==Object.prototype)throw new ExtensionError('invalid-input')
149
+ }
150
+ if(Object.keys(bundle.schemaFiles).length>256)throw new ExtensionError('invalid-input')
151
+ for(const [name,bytes] of Object.entries(bundle.schemaFiles)) {
152
+ if(!/^schemas\/[A-Za-z0-9._-]+\.json$/.test(name)||typeof bytes!=='string'||new TextEncoder().encode(bytes).length>65536)throw new ExtensionError('invalid-input')
153
+ let schema
154
+ try{schema=JSON.parse(bytes)}catch{throw new ExtensionError('invalid-input')}
155
+ if(typeof schema!=='boolean'&&(!schema||typeof schema!=='object'||Array.isArray(schema)))throw new ExtensionError('invalid-input')
156
+ canonicalJson(schema)
157
+ }
158
+ for(const [name,digest] of Object.entries(bundle.migrationDigests)) {
159
+ if(!/^migrations\/[A-Za-z0-9._/-]+$/.test(name)||name.split('/').some(part=>!part||part==='.'||part==='..')||typeof digest!=='string'||!/^[a-f0-9]{64}$/.test(digest))throw new ExtensionError('invalid-input')
160
+ }
161
+ for(const action of bundle.manifest.actions)for(const field of ['inputSchemaRef','outputSchemaRef'])if(!Object.hasOwn(bundle.schemaFiles,action[field]))throw new ExtensionError('invalid-input')
162
+ return bundle
163
+ }
@@ -0,0 +1,24 @@
1
+ import type { GenericDataModel, GenericMutationCtx, GenericQueryCtx } from 'convex/server'
2
+ import type { ExtensionManifest, Json, OperationReceipt, ProviderOperationProof } from './contracts.mjs'
3
+ import type { VerifiedScope } from './server.mjs'
4
+ export interface ConvexVerifierOptions {
5
+ issuer: string; audience: string; installationId: string; releaseDigest: string;
6
+ keys: ReadonlyMap<string, JsonWebKey>; now?: () => number;
7
+ consume?(claims: Readonly<Record<string, unknown>>, delegation: string): Promise<boolean>;
8
+ }
9
+ export interface ConvexActionDefinition<I = Json, O = Json, D extends GenericDataModel = GenericDataModel> {
10
+ input(value: unknown): I; output(value: unknown): O;
11
+ run(context: { context: GenericMutationCtx<D>; scope: VerifiedScope }, input: I): unknown | Promise<unknown>;
12
+ }
13
+ export interface ConvexExtension { readonly manifest: ExtensionManifest }
14
+ export function convexDigest(value: unknown): Promise<string>
15
+ export function assertConvexScope(scope: VerifiedScope): void
16
+ export function defineConvexExtension<D extends GenericDataModel = GenericDataModel>(options: {
17
+ manifest: ExtensionManifest; verifier: ConvexVerifierOptions; actions: Record<string, ConvexActionDefinition<any, any, D>>;
18
+ }): ConvexExtension
19
+ export function createRootAdmissionConsumer(options: { origin: string; allowLocalTesting?: boolean; fetch?: typeof fetch }): NonNullable<ConvexVerifierOptions['consume']>
20
+ export function createConvexHttpHandler(extension: ConvexExtension, invoke: (args: { delegation: string; payload: Json }) => Promise<OperationReceipt>): (request: Request) => Promise<Response>
21
+ export function runConvexInvocation<D extends GenericDataModel>(extension: ConvexExtension, context: GenericMutationCtx<D>, args: { delegation: string; payload: Json }): Promise<OperationReceipt>
22
+ export function onceConvexOperation<D extends GenericDataModel>(context: GenericMutationCtx<D>, scope: VerifiedScope, input: Json, run: (context: GenericMutationCtx<D>) => Promise<Json>): Promise<Json>
23
+ export function lookupConvexOperation<D extends GenericDataModel>(context: GenericMutationCtx<D>, scope: VerifiedScope, operationId: string): Promise<ProviderOperationProof | null>
24
+ export function convexHealth<D extends GenericDataModel>(context: GenericQueryCtx<D>, manifest: ExtensionManifest): Promise<{ schema: 'mnstry.extension-health/v1'; ready: true; profile: 'convex-native'; releaseDigest: string; schemaDigest: string }>
@@ -0,0 +1,189 @@
1
+ // Native Convex functions use Web Crypto and the existing delegation protocol.
2
+ // This module imports no Node runtime, database driver or Convex admin client.
3
+ import { canonicalJson, exact, ExtensionError, parseManifest, parseOperationReceipt, text } from './contracts.mjs'
4
+ import { assertDelegationBinding, decodeBase64Url, decodeDelegation, delegationScope, unauthorized, validateDelegationClaims } from './delegation.mjs'
5
+ import { createAdmissionConsumer, readInvocationBody } from './admission.mjs'
6
+
7
+ // Convex bundles may replace NODE_ENV at build time. Authorize the optional
8
+ // loopback test transport only when BOTH provider-owned deployment URLs are local.
9
+ export function createRootAdmissionConsumer(options) {
10
+ const localRuntime = ['CONVEX_CLOUD_URL', 'CONVEX_SITE_URL'].every(name => {
11
+ try {
12
+ const url = new URL(process.env[name])
13
+ return url.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(url.hostname) &&
14
+ !url.username && !url.password && url.pathname === '/' && !url.search && !url.hash
15
+ } catch { return false }
16
+ })
17
+ return createAdmissionConsumer(options, options.allowLocalTesting === true && localRuntime)
18
+ }
19
+
20
+ const definitions = new WeakMap()
21
+ const scopes = new WeakMap()
22
+ const encoder = new TextEncoder()
23
+ const freeze = value => {
24
+ if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value) }
25
+ return value
26
+ }
27
+ export async function convexDigest(value) {
28
+ const bytes = await crypto.subtle.digest('SHA-256', encoder.encode(canonicalJson(value)))
29
+ return [...new Uint8Array(bytes)].map(byte => byte.toString(16).padStart(2, '0')).join('')
30
+ }
31
+ export function assertConvexScope(scope) {
32
+ if (!scope || !scopes.get(scope)?.()) return unauthorized()
33
+ }
34
+ export function defineConvexExtension({ manifest, verifier, actions }) {
35
+ const pinned = parseManifest(manifest)
36
+ if (pinned.deployment.profile !== 'convex-native' || pinned.actions.some(action => !Object.hasOwn(actions, action.id)) ||
37
+ Object.keys(actions).some(id => !pinned.actions.some(action => action.id === id))) throw new ExtensionError('invalid-input')
38
+ const keys = new Map()
39
+ for (const [id, value] of verifier.keys) {
40
+ text(id)
41
+ const jwk = exact(JSON.parse(canonicalJson(value)), ['kty', 'crv', 'x', 'y'], ['alg', 'use', 'key_ops', 'ext', 'kid'])
42
+ if (jwk.kty !== 'EC' || jwk.crv !== 'P-256' || (jwk.alg !== undefined && jwk.alg !== 'ES256') ||
43
+ (jwk.use !== undefined && jwk.use !== 'sig') || (jwk.key_ops !== undefined && (jwk.key_ops.length !== 1 || jwk.key_ops[0] !== 'verify')) ||
44
+ decodeBase64Url(jwk.x).length !== 32 || decodeBase64Url(jwk.y).length !== 32) throw new ExtensionError('invalid-input')
45
+ keys.set(id, freeze(jwk))
46
+ }
47
+ const normalized = Object.fromEntries(Object.entries(actions).map(([id, action]) => {
48
+ if (['input', 'output', 'run'].some(key => typeof action[key] !== 'function')) throw new ExtensionError('invalid-input')
49
+ return [id, Object.freeze({ input: action.input, output: action.output, run: action.run })]
50
+ }))
51
+ const runtime = Object.freeze({ manifest: freeze(pinned), verifier: Object.freeze({ ...verifier, keys }), actions: Object.freeze(normalized) })
52
+ const handle = Object.freeze({ manifest: runtime.manifest })
53
+ definitions.set(handle, runtime)
54
+ return handle
55
+ }
56
+ function definition(handle) {
57
+ const extension = definitions.get(handle)
58
+ if (!extension) throw new ExtensionError('invalid-input')
59
+ return extension
60
+ }
61
+ async function invocation(extension, delegation, payload) {
62
+ let claims
63
+ try {
64
+ const decoded = decodeDelegation(delegation)
65
+ const jwk = extension.verifier.keys.get(decoded.header.kid)
66
+ if (!jwk) return unauthorized()
67
+ const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])
68
+ if (!await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decoded.signature, decoded.signed)) return unauthorized()
69
+ claims = validateDelegationClaims(decoded.claims, extension.verifier)
70
+ assertDelegationBinding(claims, extension.manifest, extension.verifier)
71
+ } catch { return unauthorized() }
72
+ const { releaseDigest: ignored, ...manifestBody } = extension.manifest
73
+ if (await convexDigest(manifestBody) !== extension.manifest.releaseDigest) throw new ExtensionError('invalid-input')
74
+ const declared = extension.manifest.actions.find(action => action.id === claims.actionId)
75
+ const action = Object.hasOwn(extension.actions, claims.actionId) ? extension.actions[claims.actionId] : null
76
+ if (!declared || !action) return unauthorized()
77
+ const encoded = canonicalJson(payload)
78
+ if (encoder.encode(encoded).length > 65536 || await convexDigest(payload) !== claims.payloadDigest) throw new ExtensionError('invalid-input')
79
+ const input = action.input(JSON.parse(encoded))
80
+ if (await convexDigest(input) !== claims.payloadDigest) throw new ExtensionError('invalid-input')
81
+ return { claims, declared, action, input }
82
+ }
83
+ function boundedResult(value, lookup = false) {
84
+ const encoded = canonicalJson(value)
85
+ if (encoder.encode(encoded).length > (lookup ? 65536 : 60 * 1024)) throw new ExtensionError('invalid-input')
86
+ // Leave the same nesting headroom required by the host's result/proof wrappers.
87
+ canonicalJson(lookup ? { result: value } : { result: { result: value } })
88
+ return JSON.parse(encoded)
89
+ }
90
+ /** Call ONLY from a registered internalMutation. Never accept a serialized scope.
91
+ * Convex's transaction atomically commits all native reads/writes or rolls back.
92
+ * Date.now is Convex's transaction-start time; this does not revoke an in-flight commit. */
93
+ export async function runConvexInvocation(handle, context, args) {
94
+ exact(args, ['delegation', 'payload'])
95
+ const extension = definition(handle)
96
+ const { claims, declared, action, input } = await invocation(extension, args.delegation, args.payload)
97
+ const scope = delegationScope(claims)
98
+ const deadline = Math.min(claims.exp * 1000, (extension.verifier.now?.() ?? Date.now()) + declared.timeoutMs)
99
+ scopes.set(scope, () => deadline > (extension.verifier.now?.() ?? Date.now()))
100
+ try {
101
+ assertConvexScope(scope)
102
+ const lookup = claims.actionId === extension.manifest.recovery?.lookupActionId
103
+ const original = canonicalJson(boundedResult(await action.run({ context, scope }, input), lookup))
104
+ const result = boundedResult(action.output(JSON.parse(original)), lookup)
105
+ // Output validators may refuse a value, but cannot silently change the
106
+ // durable result that onceConvexOperation will expose during recovery.
107
+ if (canonicalJson(result) !== original) throw new ExtensionError('invalid-input')
108
+ const resultDigest = await convexDigest(result)
109
+ assertConvexScope(scope)
110
+ return { operationId: claims.operationId, state: 'succeeded', result, resultDigest }
111
+ } finally { scopes.delete(scope) }
112
+ }
113
+ /** HTTP action: check original delegation, consume Root admission once, then call
114
+ * the private native mutation with the ORIGINAL token and canonical payload. */
115
+ export function createConvexHttpHandler(handle, invoke) {
116
+ const extension = definition(handle)
117
+ if (typeof invoke !== 'function' || typeof extension.verifier.consume !== 'function') throw new ExtensionError('invalid-input')
118
+ return async request => {
119
+ let admitted = false
120
+ try {
121
+ if (request.method !== 'POST' || request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') return new Response(null, { status: 415 })
122
+ const delegation = request.headers.get('authorization')?.match(/^Bearer ([A-Za-z0-9_.-]+)$/)?.[1]
123
+ if (!delegation) return unauthorized()
124
+ const { claims, declared, input } = await invocation(extension, delegation, await readInvocationBody(request))
125
+ if (!await extension.verifier.consume(claims, delegation)) throw new ExtensionError('forbidden', 403)
126
+ admitted = true
127
+ const deadline = Math.min(claims.exp * 1000, (extension.verifier.now?.() ?? Date.now()) + declared.timeoutMs)
128
+ if (deadline <= (extension.verifier.now?.() ?? Date.now())) throw new ExtensionError('operation-unknown', 409)
129
+ const receipt = parseOperationReceipt(await invoke({ delegation, payload: input }))
130
+ if (receipt.state !== 'succeeded' || receipt.operationId !== claims.operationId || await convexDigest(receipt.result) !== receipt.resultDigest ||
131
+ deadline <= (extension.verifier.now?.() ?? Date.now())) throw new ExtensionError('operation-unknown', 409)
132
+ boundedResult(receipt.result, claims.actionId === extension.manifest.recovery?.lookupActionId)
133
+ return Response.json(receipt, { headers: { 'cache-control': 'no-store' } })
134
+ } catch (error) {
135
+ const statuses = { 'invalid-input': 400, unauthorized: 401, forbidden: 403, 'operation-unknown': 409, unavailable: 503 }
136
+ const code = admitted ? 'operation-unknown' : error instanceof ExtensionError && Object.hasOwn(statuses, error.code) ? error.code : 'unavailable'
137
+ return Response.json({ code }, { status: statuses[code], headers: { 'cache-control': 'no-store' } })
138
+ }
139
+ }
140
+ }
141
+ function scopedQuery(context, table, index, scope, operationId) {
142
+ assertConvexScope(scope)
143
+ return context.db.query(table).withIndex(index, query => {
144
+ const scoped = query.eq('installationId', scope.installationId).eq('tenantRef', scope.tenantRef).eq('subjectRef', scope.subjectRef)
145
+ return operationId === undefined ? scoped : scoped.eq('operationId', operationId)
146
+ })
147
+ }
148
+ /** Native indexed read/OCC conflict plus result insert, within ONE internal mutation. */
149
+ export async function onceConvexOperation(context, scope, input, run) {
150
+ assertConvexScope(scope)
151
+ if (await convexDigest(input) !== scope.payloadDigest) throw new ExtensionError('invalid-input')
152
+ const row = await scopedQuery(context, 'extensionOperations', 'by_scope_operation', scope, scope.operationId).unique()
153
+ assertConvexScope(scope)
154
+ if (row) {
155
+ if (row.actionId !== scope.actionId || row.generation !== scope.generation || row.releaseDigest !== scope.releaseDigest || row.payloadDigest !== scope.payloadDigest) throw new ExtensionError('idempotency-conflict', 409)
156
+ if (await convexDigest(row.result) !== row.resultDigest) throw new ExtensionError('unavailable', 503)
157
+ return boundedResult(row.result)
158
+ }
159
+ const result = boundedResult(await run(context))
160
+ const resultDigest = await convexDigest(result)
161
+ assertConvexScope(scope)
162
+ const { rootWrites: omitted, ...identity } = scope
163
+ await context.db.insert('extensionOperations', { ...identity, result, resultDigest })
164
+ assertConvexScope(scope)
165
+ return result
166
+ }
167
+ export async function lookupConvexOperation(context, scope, operationId) {
168
+ assertConvexScope(scope)
169
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(text(operationId))) throw new ExtensionError('invalid-input')
170
+ const row = await scopedQuery(context, 'extensionOperations', 'by_scope_operation', scope, operationId).unique()
171
+ assertConvexScope(scope)
172
+ if (!row) return null
173
+ if (await convexDigest(row.result) !== row.resultDigest) throw new ExtensionError('unavailable', 503)
174
+ return { schema: 'mnstry.extension-provider-operation/v1', installationId: scope.installationId,
175
+ operationId: row.operationId, actionId: row.actionId, generation: row.generation, releaseDigest: row.releaseDigest,
176
+ payloadDigest: row.payloadDigest, resultDigest: row.resultDigest, result: row.result }
177
+ }
178
+
179
+ /** Read-only native index readiness; the existing host health envelope carries
180
+ * only immutable release/schema identities, never member rows or row counts. */
181
+ export async function convexHealth(context, value) {
182
+ const manifest = parseManifest(value)
183
+ const { releaseDigest: ignored, ...body } = manifest
184
+ if (manifest.deployment.profile !== 'convex-native' || await convexDigest(body) !== manifest.releaseDigest) throw new ExtensionError('unavailable', 503)
185
+ await context.db.query('extensionCounters').withIndex('by_scope').take(0)
186
+ await context.db.query('extensionOperations').withIndex('by_scope_operation').take(0)
187
+ return { schema: 'mnstry.extension-health/v1', ready: true, profile: 'convex-native',
188
+ releaseDigest: manifest.releaseDigest, schemaDigest: manifest.deployment.schemaDigest }
189
+ }
@@ -0,0 +1,54 @@
1
+ // One delegation contract shared by Node and provider-native runtimes.
2
+ import { exact, ExtensionError, text } from './contracts.mjs'
3
+
4
+ export const unauthorized = () => { throw new ExtensionError('unauthorized', 401) }
5
+ const claimNames = ['iss', 'aud', 'iat', 'nbf', 'exp', 'jti', 'purpose', 'installationId', 'tenantRef', 'subjectRef', 'generation', 'releaseDigest', 'operationId', 'actionId', 'payloadDigest', 'rootWrites']
6
+ const opaque = value => /^[A-Za-z0-9_-]{16,128}$/.test(text(value))
7
+ export function decodeBase64Url(part) {
8
+ if (typeof part !== 'string' || !/^[A-Za-z0-9_-]+$/.test(part)) return unauthorized()
9
+ const binary = atob(part.replace(/-/g, '+').replace(/_/g, '/'))
10
+ if (btoa(binary).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') !== part) return unauthorized()
11
+ return Uint8Array.from(binary, char => char.charCodeAt(0))
12
+ }
13
+ function decodeJson(part) {
14
+ const bytes = decodeBase64Url(part)
15
+ const value = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
16
+ const encoded = new TextEncoder().encode(value)
17
+ if (encoded.length !== bytes.length || encoded.some((byte, index) => byte !== bytes[index])) return unauthorized()
18
+ return JSON.parse(value)
19
+ }
20
+ export function decodeDelegation(delegation) {
21
+ text(delegation, 16384)
22
+ const parts = delegation.split('.')
23
+ if (parts.length !== 3) return unauthorized()
24
+ const header = exact(decodeJson(parts[0]), ['alg', 'typ', 'kid'])
25
+ if (header.alg !== 'ES256' || header.typ !== 'JWT') return unauthorized()
26
+ text(header.kid)
27
+ const signature = decodeBase64Url(parts[2])
28
+ if (signature.length !== 64) return unauthorized()
29
+ return { header, claims: decodeJson(parts[1]), signature, signed: new TextEncoder().encode(`${parts[0]}.${parts[1]}`) }
30
+ }
31
+ export function validateDelegationClaims(value, options) {
32
+ const claims = exact(value, claimNames)
33
+ const now = Math.floor((options.now?.() ?? Date.now()) / 1000)
34
+ if (claims.iss !== options.issuer || claims.aud !== options.audience || claims.purpose !== 'extension.invoke/v1' ||
35
+ ![claims.iat, claims.nbf, claims.exp].every(Number.isSafeInteger) || claims.iat > now || claims.nbf !== claims.iat ||
36
+ claims.exp <= now || claims.exp <= claims.iat || claims.exp - claims.iat > 60) return unauthorized()
37
+ for (const field of ['jti', 'installationId', 'operationId', 'tenantRef', 'subjectRef']) if (!opaque(claims[field])) return unauthorized()
38
+ text(claims.actionId)
39
+ if (typeof claims.generation !== 'string' || typeof claims.releaseDigest !== 'string' || typeof claims.payloadDigest !== 'string' || !/^[1-9][0-9]{0,18}$/.test(claims.generation) || BigInt(claims.generation) > 9223372036854775807n ||
40
+ !/^[a-f0-9]{64}$/.test(claims.releaseDigest) || !/^[a-f0-9]{64}$/.test(claims.payloadDigest) ||
41
+ !Array.isArray(claims.rootWrites) || claims.rootWrites.length > 64 || new Set(claims.rootWrites).size !== claims.rootWrites.length) return unauthorized()
42
+ claims.rootWrites.forEach(value => text(value, 256))
43
+ return Object.freeze({ ...claims, rootWrites: Object.freeze([...claims.rootWrites]) })
44
+ }
45
+ export function assertDelegationBinding(claims, manifest, verifier) {
46
+ if (claims.installationId !== verifier.installationId || claims.releaseDigest !== manifest.releaseDigest ||
47
+ claims.releaseDigest !== verifier.releaseDigest || claims.rootWrites.length !== manifest.permissions.rootWrites.length ||
48
+ claims.rootWrites.some(value => !manifest.permissions.rootWrites.includes(value))) return unauthorized()
49
+ }
50
+ export function delegationScope(claims) {
51
+ return Object.freeze({ installationId: claims.installationId, generation: claims.generation, releaseDigest: claims.releaseDigest,
52
+ operationId: claims.operationId, actionId: claims.actionId, payloadDigest: claims.payloadDigest,
53
+ tenantRef: claims.tenantRef, subjectRef: claims.subjectRef, rootWrites: claims.rootWrites })
54
+ }
@@ -0,0 +1,11 @@
1
+ import type { Sql } from 'postgres'
2
+ import type { Json, ProviderOperationProof } from './contracts.mjs'
3
+ import type { VerifiedScope } from './server.mjs'
4
+ export interface DrizzleTransaction { execute(query: any): PromiseLike<any> }
5
+ /** Native drizzle-orm/postgres-js database; other drivers are unsupported. */
6
+ export interface DrizzleDatabase<Transaction extends DrizzleTransaction> { readonly $client: Sql; transaction<T>(run:(transaction:Transaction)=>Promise<T>):Promise<T> }
7
+ export type DrizzleSqlTag = (chunks:TemplateStringsArray,...values:any[])=>unknown
8
+ export function withDrizzleScope<Transaction extends DrizzleTransaction,T>(database: DrizzleDatabase<Transaction>, scope: VerifiedScope, sql: DrizzleSqlTag, run: (transaction: Transaction) => Promise<T>): Promise<T>
9
+ export function onceDrizzleOperation<Transaction extends DrizzleTransaction>(database: DrizzleDatabase<Transaction>, scope: VerifiedScope, input: Json, sql: DrizzleSqlTag, run: (transaction: Transaction) => Promise<Json>): Promise<Json>
10
+ export function lookupDrizzleOperation<Transaction extends DrizzleTransaction>(database:DrizzleDatabase<Transaction>,scope:VerifiedScope,sql:DrizzleSqlTag,operationId:string):Promise<ProviderOperationProof|null>
11
+ export function drizzleHealth(driver: Sql, profile: 'drizzle-db-first'|'drizzle-code-first'): Promise<{ ready: true; profile: 'drizzle-db-first'|'drizzle-code-first'; postgresMajor: 17 }>
@@ -0,0 +1,33 @@
1
+ import { withPostgresScope, oncePostgresOperation, lookupPostgresOperation, postgresHealth } from './postgres.mjs'
2
+ import { ExtensionError } from './contracts.mjs'
3
+
4
+ // The native ORM owns the transaction. The application supplies its SQL tag;
5
+ // Atelier never imports the optional ORM or reconstructs it around a driver tx.
6
+ function bridge(database, sqlTag) {
7
+ if(typeof database?.transaction!=='function'||typeof sqlTag!=='function')throw new ExtensionError('invalid-input')
8
+ const transactions=new WeakMap()
9
+ return { transactions, database:{begin:callback=>database.transaction(tx=>{
10
+ const query=(chunks,...values)=>tx.execute(sqlTag(chunks,...values))
11
+ query.json=value=>JSON.stringify(value)
12
+ transactions.set(query,tx)
13
+ return callback(query)
14
+ })} }
15
+ }
16
+ export async function withDrizzleScope(database, scope, sqlTag, run) {
17
+ if(typeof run!=='function')throw new ExtensionError('invalid-input')
18
+ const native=bridge(database,sqlTag)
19
+ return withPostgresScope(native.database,scope,query=>run(native.transactions.get(query)))
20
+ }
21
+ export async function onceDrizzleOperation(database, scope, input, sqlTag, run) {
22
+ if(typeof run!=='function')throw new ExtensionError('invalid-input')
23
+ const native=bridge(database,sqlTag)
24
+ return oncePostgresOperation(native.database,scope,input,query=>run(native.transactions.get(query)))
25
+ }
26
+ export async function lookupDrizzleOperation(database,scope,sqlTag,operationId) {
27
+ return lookupPostgresOperation(bridge(database,sqlTag).database,scope,operationId)
28
+ }
29
+ export async function drizzleHealth(driver, profile) {
30
+ if (!['drizzle-db-first', 'drizzle-code-first'].includes(profile)) throw new ExtensionError('invalid-input')
31
+ const health = await postgresHealth(driver)
32
+ return { ...health, profile }
33
+ }
@@ -0,0 +1,7 @@
1
+ import type { Sql, TransactionSql } from 'postgres'
2
+ import type { Json, ProviderOperationProof } from './contracts.mjs'
3
+ import type { VerifiedScope } from './server.mjs'
4
+ export function withPostgresScope<T>(database: Sql, scope: VerifiedScope, run: (sql: TransactionSql) => Promise<T>): Promise<T>
5
+ export function oncePostgresOperation(database: Sql, scope: VerifiedScope, input: Json, run: (sql: TransactionSql) => Promise<Json>): Promise<Json>
6
+ export function lookupPostgresOperation(database: Sql, scope: VerifiedScope, operationId: string): Promise<ProviderOperationProof|null>
7
+ export function postgresHealth(database: Sql): Promise<{ ready: true; profile: 'postgres-sql'; postgresMajor: 17 }>