@noodleseed/one 0.154.0 → 0.156.0

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 (154) hide show
  1. package/README.md +5 -1
  2. package/dist/assistant-embed-django-tests.d.ts +3 -0
  3. package/dist/assistant-embed-django-tests.d.ts.map +1 -0
  4. package/dist/assistant-embed-django-tests.js +140 -0
  5. package/dist/assistant-embed-django-tests.js.map +1 -0
  6. package/dist/assistant-embed-django.d.ts +4 -0
  7. package/dist/assistant-embed-django.d.ts.map +1 -0
  8. package/dist/assistant-embed-django.js +259 -0
  9. package/dist/assistant-embed-django.js.map +1 -0
  10. package/dist/assistant-embed-installation.d.ts +29 -0
  11. package/dist/assistant-embed-installation.d.ts.map +1 -0
  12. package/dist/assistant-embed-installation.js +124 -0
  13. package/dist/assistant-embed-installation.js.map +1 -0
  14. package/dist/assistant-embed-profile.d.ts +8 -0
  15. package/dist/assistant-embed-profile.d.ts.map +1 -0
  16. package/dist/assistant-embed-profile.js +150 -0
  17. package/dist/assistant-embed-profile.js.map +1 -0
  18. package/dist/assistant-embed-scaffold-template.d.ts +3 -2
  19. package/dist/assistant-embed-scaffold-template.d.ts.map +1 -1
  20. package/dist/assistant-embed-scaffold-template.js +39 -84
  21. package/dist/assistant-embed-scaffold-template.js.map +1 -1
  22. package/dist/assistant-embed-tests.d.ts +5 -0
  23. package/dist/assistant-embed-tests.d.ts.map +1 -0
  24. package/dist/assistant-embed-tests.js +106 -0
  25. package/dist/assistant-embed-tests.js.map +1 -0
  26. package/dist/assistant-embed-vue.d.ts +3 -0
  27. package/dist/assistant-embed-vue.d.ts.map +1 -0
  28. package/dist/assistant-embed-vue.js +77 -0
  29. package/dist/assistant-embed-vue.js.map +1 -0
  30. package/dist/commands/assistant-embed-ops.d.ts.map +1 -1
  31. package/dist/commands/assistant-embed-ops.js +112 -63
  32. package/dist/commands/assistant-embed-ops.js.map +1 -1
  33. package/dist/commands/author-loop.d.ts.map +1 -1
  34. package/dist/commands/author-loop.js +24 -28
  35. package/dist/commands/author-loop.js.map +1 -1
  36. package/dist/commands/deploy-first-flow.d.ts.map +1 -1
  37. package/dist/commands/deploy-first-flow.js +26 -15
  38. package/dist/commands/deploy-first-flow.js.map +1 -1
  39. package/dist/commands/deploy-preflight.d.ts +5 -0
  40. package/dist/commands/deploy-preflight.d.ts.map +1 -1
  41. package/dist/commands/deploy-preflight.js +44 -5
  42. package/dist/commands/deploy-preflight.js.map +1 -1
  43. package/dist/commands/local-readiness.d.ts +9 -1
  44. package/dist/commands/local-readiness.d.ts.map +1 -1
  45. package/dist/commands/local-readiness.js +74 -4
  46. package/dist/commands/local-readiness.js.map +1 -1
  47. package/dist/commands/project-setup.d.ts.map +1 -1
  48. package/dist/commands/project-setup.js +10 -15
  49. package/dist/commands/project-setup.js.map +1 -1
  50. package/dist/control-plane-request.d.ts +4 -0
  51. package/dist/control-plane-request.d.ts.map +1 -1
  52. package/dist/control-plane-request.js +40 -20
  53. package/dist/control-plane-request.js.map +1 -1
  54. package/dist/project-scaffold-model.d.ts +14 -0
  55. package/dist/project-scaffold-model.d.ts.map +1 -0
  56. package/dist/project-scaffold-model.js +36 -0
  57. package/dist/project-scaffold-model.js.map +1 -0
  58. package/dist/project-scaffold-templates.d.ts.map +1 -1
  59. package/dist/project-scaffold-templates.js +73 -39
  60. package/dist/project-scaffold-templates.js.map +1 -1
  61. package/dist/project-scaffold-test-template.d.ts +4 -0
  62. package/dist/project-scaffold-test-template.d.ts.map +1 -0
  63. package/dist/project-scaffold-test-template.js +89 -0
  64. package/dist/project-scaffold-test-template.js.map +1 -0
  65. package/dist/project.d.ts.map +1 -1
  66. package/dist/project.js +1 -2
  67. package/dist/project.js.map +1 -1
  68. package/dist/widget-focused-template.d.ts +1 -1
  69. package/dist/widget-focused-template.d.ts.map +1 -1
  70. package/dist/widget-focused-template.js +34 -12
  71. package/dist/widget-focused-template.js.map +1 -1
  72. package/dist/widget-scaffold-template.d.ts +1 -1
  73. package/dist/widget-scaffold-template.d.ts.map +1 -1
  74. package/dist/widget-scaffold-template.js +75 -43
  75. package/dist/widget-scaffold-template.js.map +1 -1
  76. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +18 -4
  77. package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.js +2 -2
  78. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +9 -12
  79. package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js +2 -2
  80. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-operations-ref.js +1 -1
  81. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +34 -26
  82. package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.js +2 -2
  83. package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +2 -1
  84. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  85. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-suggestions.js +5 -3
  86. package/node_modules/@noodle-borg/assistant-gateway/dist/continuity-bounds.d.ts +36 -0
  87. package/node_modules/@noodle-borg/assistant-gateway/dist/continuity-bounds.js +52 -0
  88. package/node_modules/@noodle-borg/assistant-gateway/dist/continuity-store.d.ts +115 -0
  89. package/node_modules/@noodle-borg/assistant-gateway/dist/continuity-store.js +51 -0
  90. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-continuity-store.d.ts +26 -0
  91. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-continuity-store.js +80 -0
  92. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +1 -0
  93. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +2 -0
  94. package/node_modules/@noodle-borg/assistant-gateway/dist/model-response-values.js +11 -0
  95. package/node_modules/@noodle-borg/assistant-gateway/dist/model-responses.js +1 -10
  96. package/node_modules/@noodle-borg/assistant-gateway/dist/model-stream.js +1 -10
  97. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +3 -0
  98. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +3 -0
  99. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  100. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +41 -0
  101. package/node_modules/@noodle-borg/authoring/dist/assistant.js +4 -0
  102. package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +1 -1
  103. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +10 -4
  104. package/node_modules/@noodle-borg/compiler/dist/manifest/branding-schema.js +86 -0
  105. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +44 -25
  106. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +24 -76
  107. package/node_modules/@noodle-borg/compiler/package.json +0 -1
  108. package/node_modules/@noodle-borg/deploy-client/dist/deploy-payload-size.js +6 -2
  109. package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +1 -1
  110. package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +193 -96
  111. package/node_modules/@noodle-borg/service/package.json +1 -1
  112. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +3 -0
  113. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +3 -0
  114. package/node_modules/@noodleseed/assistant/README.md +21 -23
  115. package/node_modules/@noodleseed/assistant/package.json +1 -1
  116. package/package.json +5 -4
  117. package/dist/saas-scaffold-template.d.ts +0 -4
  118. package/dist/saas-scaffold-template.d.ts.map +0 -1
  119. package/dist/saas-scaffold-template.js +0 -56
  120. package/dist/saas-scaffold-template.js.map +0 -1
  121. package/node_modules/tailwindcss/LICENSE +0 -21
  122. package/node_modules/tailwindcss/README.md +0 -36
  123. package/node_modules/tailwindcss/dist/chunk-5JIJA4QV.mjs +0 -1
  124. package/node_modules/tailwindcss/dist/chunk-HMCCH6MG.mjs +0 -1
  125. package/node_modules/tailwindcss/dist/chunk-X4GG3EDV.mjs +0 -1
  126. package/node_modules/tailwindcss/dist/colors-C__qRT83.d.ts +0 -347
  127. package/node_modules/tailwindcss/dist/colors.d.mts +0 -347
  128. package/node_modules/tailwindcss/dist/colors.d.ts +0 -5
  129. package/node_modules/tailwindcss/dist/colors.js +0 -1
  130. package/node_modules/tailwindcss/dist/colors.mjs +0 -1
  131. package/node_modules/tailwindcss/dist/default-theme.d.mts +0 -1199
  132. package/node_modules/tailwindcss/dist/default-theme.d.ts +0 -1199
  133. package/node_modules/tailwindcss/dist/default-theme.js +0 -1
  134. package/node_modules/tailwindcss/dist/default-theme.mjs +0 -1
  135. package/node_modules/tailwindcss/dist/flatten-color-palette.d.mts +0 -6
  136. package/node_modules/tailwindcss/dist/flatten-color-palette.d.ts +0 -6
  137. package/node_modules/tailwindcss/dist/flatten-color-palette.js +0 -1
  138. package/node_modules/tailwindcss/dist/flatten-color-palette.mjs +0 -1
  139. package/node_modules/tailwindcss/dist/lib.d.mts +0 -379
  140. package/node_modules/tailwindcss/dist/lib.d.ts +0 -3
  141. package/node_modules/tailwindcss/dist/lib.js +0 -38
  142. package/node_modules/tailwindcss/dist/lib.mjs +0 -38
  143. package/node_modules/tailwindcss/dist/plugin.d.mts +0 -11
  144. package/node_modules/tailwindcss/dist/plugin.d.ts +0 -134
  145. package/node_modules/tailwindcss/dist/plugin.js +0 -1
  146. package/node_modules/tailwindcss/dist/plugin.mjs +0 -1
  147. package/node_modules/tailwindcss/dist/resolve-config-B4yBzhca.d.ts +0 -29
  148. package/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts +0 -190
  149. package/node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts +0 -128
  150. package/node_modules/tailwindcss/index.css +0 -944
  151. package/node_modules/tailwindcss/package.json +0 -89
  152. package/node_modules/tailwindcss/preflight.css +0 -393
  153. package/node_modules/tailwindcss/theme.css +0 -510
  154. package/node_modules/tailwindcss/utilities.css +0 -1
@@ -29,14 +29,14 @@ export function renderBuildAnMcpAppReference() {
29
29
  '4. **Preserve fallback.** Every tool that launches a widget must still return useful text without the widget, so unsupported hosts and failed rendering remain usable.',
30
30
  '5. **Decide product-guide coverage.** Record the required product-guide decision and its reason, then use `references/product-agent-guides.md` as the canonical selection and authoring guidance.',
31
31
  '6. **Author and wire the App contract.** Follow `references/widgets-and-apps.md` for the canonical component guidance, view registration, hooks, state, CSP, tool visibility, and output shaping. Keep tool effects and confirmation semantics correct independently of the UI.',
32
- '7. **Validate the local artifact.** Run `noodle validate --json`, `noodle test --json`, and `noodle check --json`. Repair failures at the layer that produced them.',
32
+ '7. **Validate the local artifact.** Run the generated/adapted `npm test`, `noodle validate --json` and `noodle check --json`. Prove a representative result and negative case, not just registration. Synthetic preference previews do not save; replace their backend seam and verify the authorized effect before showing success.',
33
33
  '8. **Inspect the experience.** Run `noodle devtools` and verify loading, empty, error, success, responsive layout, focus/keyboard behavior, and the text fallback.',
34
34
  '9. **Escalate evidence only on request.** Run a host test only when the user requested host verification. Run host-specific compliance only when preparing that host submission; select the exact host-testing or compliance entry from the router lookup catalog only after that evidence level is explicitly requested.',
35
35
  '',
36
36
  '## Verification evidence',
37
37
  '',
38
38
  '- **Product:** the design spec states the user benefit, UI fit decision, and product-guide decision with its reason.',
39
- '- **Server:** `noodle validate --json` and `noodle test --json` succeeded.',
39
+ '- **Server:** compilation, a representative call and negative input/authorization cases passed; report whether fixtures or a real authorized backend were used.',
40
40
  '- **App contract:** `noodle check --json` succeeded.',
41
41
  '- **Local UX:** `noodle devtools` exercised the relevant states and the useful text fallback without the widget.',
42
42
  '- **Host/compliance:** report each requested host or compliance check with its evidence; report every unperformed higher level as not run.',
@@ -205,20 +205,14 @@ export function renderAuthoringWorkflowReference() {
205
205
  '',
206
206
  '## Tests',
207
207
  '',
208
- 'Use Vitest for app-local tests. The generated `npm test` command scans only the project-owned `test/` directory; skill-local example tests are reference material, not part of the app suite. Keep fixtures project-local; do not import from `examples/`. A minimum test suite imports the default server, checks the intended definitions compile, then lets `noodle test --json` perform the loopback MCP smoke.',
208
+ 'Start from the generated `test/server.test.ts`, not a blank test file. `npm test` runs `vitest run --dir test` over the project-owned `test/` directory; skill-local example tests are reference material. The generated suite compiles the actual source/view, lists tools, asserts a representative result and rejects invalid input. It copies source into temporary fixture storage and uses synthetic configuration, never customer secrets or saved hosted targets. The HTTP profile proves one local backend read and zero extra calls for invalid input.',
209
209
  '',
210
- '```ts',
211
- "import { describe, expect, it } from 'vitest';",
212
- "import app from '../src/server.js';",
213
- '',
214
- "describe('server', () => {",
215
- " it('declares the expected tool surface', () => {",
216
- " expect(app.name).toBe('support_assistant');",
217
- ' });',
218
- '});',
210
+ '```sh',
211
+ 'npm test',
212
+ 'npm run agent:check',
219
213
  '```',
220
214
  '',
221
- 'After focused tests pass, run `noodle validate --json`, `noodle test --json`, and then `noodle dev` for interactive local verification.',
215
+ 'Adapt the representative arguments and assertions when business contracts change; keep invalid-input and authorization cases. A name/export assertion or `noodle test --json` without `--tool` proves no useful call. After fixture tests pass, bind the real local configuration, run an authorized representative read, and use `noodle dev` for interactive checks. Never use a production mutation or fixture success as customer-readiness evidence.',
222
216
  '',
223
217
  '## Secrets and variables',
224
218
  '',
@@ -568,7 +562,10 @@ const FLAGSHIP_EXAMPLES = [
568
562
  'customer-auth',
569
563
  'End-user OIDC, private customer API routing, route-bound confirmed actions, roles/scopes, and delegated credentials.',
570
564
  ],
571
- ['stateful-draft', 'Durable, caller-scoped widget state handles with optimistic revisions.'],
565
+ [
566
+ 'stateful-draft',
567
+ 'Review and save a brief before signup; carry caller-scoped state into an account.',
568
+ ],
572
569
  ['perplexity', 'A real SaaS API with bearer auth and a managed `secret`.'],
573
570
  ['bitcoin', 'API-key HTTP connector, custom auth header, and compute normalization.'],
574
571
  [
@@ -203,7 +203,7 @@ export function renderConnectAnApiReference() {
203
203
  '',
204
204
  '### Set the secret for local runs',
205
205
  '',
206
- 'Local `dev`, smoke commands, secrets, and variables resolve one effective target: explicit flags, then the project link, then the saved CLI target, then local defaults. Set the secret through that same target:',
206
+ 'Local authoring resolves explicit coordinates, then the complete project link, then deterministic local defaults; saved global coordinates do not redirect an unlinked project. Follow the diagnostic’s exact-target commands from the project directory for every missing secret and variable:',
207
207
  '',
208
208
  '```sh',
209
209
  '# Canonical: writes to the effective local environment used by dev/test/devtools:',
@@ -220,7 +220,7 @@ export function renderConnectAnApiReference() {
220
220
  '',
221
221
  '### Prove real output',
222
222
  '',
223
- '`noodle validate` / `noodle test` prove a connector tool *compiles and registers* — not that its',
223
+ '`noodle validate` / `noodle test` without `--tool` prove a connector tool *compiles and registers* — not that its',
224
224
  'mapping returns data. With the secret set, run a live read: `noodle tools call <read_tool> --args',
225
225
  "'{…}'` executes the connector against the real API in-process. Confirm the mapped fields are populated,",
226
226
  'not `undefined`; if they are empty, distinguish a legitimate empty result from a missing or incorrect mapping, fix `${response…}` paths against the real payload when needed, and re-run.',
@@ -11,7 +11,7 @@ export function renderEmbeddedAssistantOperationsReference() {
11
11
  'noodle assistant embed --check --json --require-env EXAMPLE_DELEG_CLIENT_SECRET',
12
12
  '```',
13
13
  '',
14
- 'The check reports only required and missing environment names, never their values. Pass `--surface public|mixed|authenticated` to match the deployment: `public` drops the backend client id/secret requirement (a public embed has neither), and `public`/`mixed` additionally require `script-src` the one directive whose failure runs no widget code at all, so nothing can report it from inside the page. CSP directives verify against the service origin exactly, via the env placeholder, or through a covering wildcard (`https://*.example.com`); a dynamic expression is marked unverified instead of guessed. Additional `--require-env` names are application-owned; `--env-alias NAME=HOST_NAME` follows a host repo that names an env var differently.',
14
+ 'The check reports required and missing environment names, never their values. Use the exact command in an installed NOODLE-INTEGRATION.md, including its aliases and additional names, with the host environment exported; the CLI does not load application dotenv files. Pass `--surface public|mixed|authenticated` to match the deployment: `public` needs no backend client credentials. Public script-tag mounts need the service in `script-src`; the generated bundled React mount needs only `connect-src` and `frame-src` for that service. Unknown public mounts retain the conservative script check. CSP verifies an exact service origin, an env placeholder or a covering wildcard; dynamic expressions stay unverified. `--require-env` adds application-owned names and `--env-alias NAME=HOST_NAME` follows existing host naming.',
15
15
  '',
16
16
  'Read `evidence.levels` in order and stop at `evidence.firstUnproven`:',
17
17
  '',
@@ -9,6 +9,7 @@ export function renderEmbeddedAssistantReference() {
9
9
  '',
10
10
  '- Architecture',
11
11
  '- Select the architecture before code',
12
+ '- Install into the existing application',
12
13
  '- Author and validate',
13
14
  '- Product workflow guidance',
14
15
  '- Customize the presentation',
@@ -68,6 +69,20 @@ export function renderEmbeddedAssistantReference() {
68
69
  '',
69
70
  'The architecture brief must state the selected topology, why it fits the named user and job, the two code owners and two local processes, credential and identity flow, routing authority, model owner, files expected to change, and the first requested evidence boundary. If identity/session ownership, exact origin, tenant-routing authority, model ownership, or evidence target is unavailable, return a blocked architecture brief naming the missing owner or decision and do not edit either codebase.',
70
71
  '',
72
+ '## Install into the existing application',
73
+ '',
74
+ 'Configure or reuse an existing capability before generating files. Public embedding uses the existing script or embed ID and needs no customer backend session/config route. Reuse application APIs and business functions; generate a thin handler only where the approved architecture identifies a missing stable boundary.',
75
+ '',
76
+ 'For a supported host, first run `noodle assistant embed --framework nextjs --surface public --dry-run --json` in the customer application, substituting `authenticated` or `mixed` only for the selected surface. Read the returned recipe applicability, applicationSeams, generated contents/hashes, skipped conflicts and nextSteps. The output is an installation plan, not permission to change the repository.',
77
+ '',
78
+ 'For the documented non-Node backend/static-frontend authenticated profile, discover the framework choice through `noodle commands --json` and follow the linked framework guide below. The installer supplies a session view, a fail-closed identity seam, real CSRF-enforced request tests, a browser wrapper and exact-destination CSRF transport tests. Keep the existing package managers, authentication middleware and same-origin proxy. Pass the existing CSRF token and user/tenant principalKey to the wrapper; the managed element supports injected fetch, so preserving CSRF does not require a custom renderer or Node production server. Unsupported combinations use the neutral contract, not a claimed qualified profile.',
79
+ '',
80
+ 'Once local changes are authorized, rerun without `--dry-run`; never add `--force` to resolve a conflict automatically. Read the installed `NOODLE-INTEGRATION.md` and newly installed project skill before editing. Implement only the named session/business/mount seams and preserve the host framework, package manager, login and authorization. Mixed mode also requires an application-owned login transaction; the generated callback does not implement or verify that transaction.',
81
+ '',
82
+ 'Use generated tests and independently maintained acceptance cases at the actual customer boundary. A static host check, a mock test or installed files cannot prove application authorization or production browser behavior. Missing identities, sandbox access, customer approval or a reachable service remain unverified. Record the exact application/Noodle revisions, package versions and environment for observed evidence; rerun affected checks after changes.',
83
+ '',
84
+ 'Unsupported hosts use the framework-neutral integration contract below; do not claim they received a generated, qualified framework profile. Keep direct MCP and headless projections on the same TypeScript source, and preserve their separate identity requirements.',
85
+ '',
71
86
  '## Author and validate',
72
87
  '',
73
88
  '`noodle init` and `noodle init --template widget` deliberately produce credential-free MCP Apps. Add an assistant declaration only when the product explicitly includes a customer-hosted assistant; do not make ordinary external-host widgets depend on model-provider settings.',
@@ -148,6 +163,16 @@ export function renderEmbeddedAssistantReference() {
148
163
  '',
149
164
  '#### Complete mixed-mode handoff',
150
165
  '',
166
+ 'For a small SaaS onboarding flow, first read the bundled [stateful-draft example](../examples/stateful-draft/README.md). Compose the existing capabilities; do not introduce an onboarding platform API, workflow engine, research pipeline, or second identity provider.',
167
+ '',
168
+ 'Start from one useful result the visitor can see before signup. Ask only for missing information, one short question at a time. Show an editable preview, and offer an account only when it unlocks a meaningful next step. Keep the ordinary signup route available. Research and document parsing are optional enrichment, never required because the flow is conversational.',
169
+ '',
170
+ 'Use a finite-TTL caller state handle only if the draft needs server persistence. Opt it into `claimOnAuthentication` when it should move to the verified account. An explicit typed read tool supplies the current revision; a typed patch tool sends that revision and the complete reviewed value. Widget state is a display cache, never proof of a save. A failed or missing result must not increment the revision, mark the draft saved, or silently retry a conflicting write.',
171
+ '',
172
+ 'Keep the final business action in the customer API. Map the reviewed draft to its existing create/update operation after authentication, with confirmation and the backend’s existing authorization and idempotency. Ticket adoption transfers temporary state, not a CRM record or completed onboarding.',
173
+ '',
174
+ 'Prove this journey before calling it ready: useful anonymous preview; actual saved state; cancelled/expired signup; verified signup with the same draft; a destination conflict without an automatic merge; and no business write merely because login succeeded. A synthetic login demonstrates the integration shape but is not evidence that the customer identity system is connected.',
175
+ '',
151
176
  '1. **Capture the ticket.** The widget raises `assistant-sign-in-requested` with a single-use `signInTicket`. POST it to the host application same-origin backend and bind it to the short-lived login transaction; keep it out of query strings, logs, analytics, and durable browser storage.',
152
177
  '2. **Use the existing login.** Send the visitor through the host application full-page redirect for sign-in or sign-up. Noodle never becomes the customer identity provider.',
153
178
  '3. **Elevate on the backend.** After authentication, recover the bound ticket and call `createAssistantSession({ ..., signInTicket })` with the verified user, server-owned routing, backend client credentials, and the exact **origin the conversation will continue on**. The allowed destination may differ from the anonymous marketing origin; successful elevation rebinds the session to it.',
@@ -202,6 +227,8 @@ export function renderEmbeddedAssistantReference() {
202
227
  '',
203
228
  "Set `webmcp: { enabled: true }` on the assistant to let a browser agent reach this session's tools through the page's WebMCP API, and set it on an individual access surface to override that default in either direction — a marketing surface can opt in while a signed-in one opts out, or the reverse. Off unless set, and inert in browsers without `document.modelContext`. It governs discovery: whether the embed registers the tools this session already projects, narrowed to those that are both app-callable and model-visible. Every call executes over the same apps-bridge path the assistant's own calls take, so a browser agent gets the session's authority and nothing more, and a `confirm: true` tool still stops for a human in the panel rather than being accepted on the agent's behalf. It is not a second authorization boundary — the session is the only one. Bridge calls spend their own per-session and per-day budgets instead of model turns, and the surface's daily kill switch stops them too. Prefer this over hand-registering page-local tools that borrow the visitor's session: those carry no scoped authority, policy, or audit trail.",
204
229
  '',
230
+ 'Set `continuity: { enabled: true }` on a public or mixed access surface to let an anonymous visitor keep the conversation they can see when they navigate to another page of the same site. Off unless set, and refused on an authenticated surface, which reattaches through a backend-verified sign-in instead. It restores the visible text on a fresh session and never the old one: no tool authority, no share of a spent turn budget, and no pending confirmation carried across, so an unanswered confirmation stays unanswered. `windowSeconds` defaults to 300 with a 600 ceiling, `maxRestores` to 3 with a ceiling of 10, and 0 for either disables continuity outright; an operator may lower what you declare and can never raise it. The handle lives in `sessionStorage` so it dies with the tab, is single-use, and is valid only for the embed, origin, and visitor it was issued to. Declare it when a marketing site spreads one conversation across several pages; leave it off when anonymous conversation text should not survive a navigation at all.',
231
+ '',
205
232
  'Give every business action a portable `tool(..., { title: "Complete task", description: "This will mark the task complete for everyone.", input: z.object({ task: z.string().meta({ title: "Task" }) }) })` title. The standard confirmation uses the tool title/description plus schema field `title`, `description`, and `format`; it shows Confirm and Don\'t proceed and keeps technical action details secondary. `behavior.showConfirmationDetails` defaults to `true`; set it to `false` to remove only the built-in card\'s Additional details disclosure and connector mechanics. The business review and decisions remain, `confirm: true` still suspends until acceptance, and headless/BYO `data-confirmation` stays unchanged. Do not put JSON or implementation names in business-facing copy.',
206
233
  '',
207
234
  '## Configure and deploy',
@@ -249,39 +276,20 @@ export function renderEmbeddedAssistantReference() {
249
276
  '',
250
277
  'Read the customer repository lockfile or `packageManager` field and install `@noodleseed/assistant` with that existing package manager; never introduce a second lockfile.',
251
278
  '',
252
- 'Create an authenticated same-origin backend route:',
279
+ 'Use the installed same-origin route and `createAssistantSessionHandler` from `@noodleseed/assistant/server`; do not regenerate guards, JSON parsing or token-exchange infrastructure. The only identity seam is `authenticateAssistantRequest` in `lib/noodle-assistant-auth.ts`.',
253
280
  '',
254
- 'The route must return a JSON `401` for a signed-out caller rather than an HTML login redirect, require JSON, and compare the request against the exact configured Origin before exchange. The managed renderer sends same-origin cookies but does not invent an application CSRF token. Treat JSON plus exact Origin as the minimum cookie/CSRF boundary; when the host keeps token-based CSRF middleware, use the DOM-free client with an injected `fetch` that supplies the token instead of exempting the route. Resolve claims and connector routes from the authenticated user and server-owned membership only.',
281
+ 'The route must return a JSON `401` for a signed-out caller rather than an HTML login redirect, require JSON, and compare the request against the exact configured Origin before exchange. The managed renderer sends same-origin cookies but does not invent an application CSRF token. Preserve the host token-based CSRF middleware: inject a same-origin-only token fetch into the managed element before mounting, as the Django/Vue profile does; a custom renderer is not required. Invalid CSRF may correctly fail before authentication. Resolve claims and connector routes from the authenticated user and server-owned membership only.',
255
282
  '',
256
283
  'For the complete framework-neutral server-to-server HTTP exchange in a non-Node host, link to the [Django and Vue guide](https://docs.noodleseed.dev/docs/guides/embedded-assistant-django-vue); keep its framework-specific middleware and mounting mechanics there rather than copying them into agent instructions.',
257
284
  '',
258
- '```ts',
259
- 'import { createAssistantSession } from "@noodleseed/assistant/server";',
260
- '',
261
- 'export async function POST(request: Request) {',
262
- ' const user = await requireCurrentUser(request);',
263
- ' const { context } = await request.json();',
264
- ' const session = await createAssistantSession({',
265
- ' serviceUrl: process.env.NOODLE_SERVICE_URL!,',
266
- ' clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,',
267
- ' clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,',
268
- ' origin: process.env.PUBLIC_APP_ORIGIN!,',
269
- ' user: {',
270
- ' id: user.id,',
271
- ' email: user.email,',
272
- ' roles: user.roles,',
273
- ' scopes: user.scopes,',
274
- ' },',
275
- ' context,',
276
- ' // Saved, backend-verified user preferences outrank browser hints.',
277
- ' preferences: { locale: user.locale, timeZone: user.timeZone },',
278
- ' });',
279
- ' return Response.json(session);',
280
- '}',
281
- '```',
285
+ 'Return `AssistantSessionIdentity | null` from the adapter. Null means signed out; service failure must throw rather than masquerade as signed out. Derive `user`, claims, preferences and routes from the existing verified session/membership. The maintained handler owns exact-Origin/JSON checks, bounded flat context, no-store JSON failures, a 15-second deadline, no redirects and no exchange retries. For mixed-mode continuation, return a `signInTicket` only from the backend-bound login transaction; never take it directly from browser JSON.',
286
+ '',
287
+ 'For saved presentation preferences, return `preferences: { locale: user.locale, timeZone: user.timeZone }` beside the verified `user`. Omit unknown values; browser hints are not verified account preferences.',
282
288
  '',
283
289
  'Authenticate before exchange. Pass backend-verified `user.roles` and OAuth-style `user.scopes` separately; they govern the same per-tool authorization rules as verified MCP bearer claims. Source `origin` from trusted server configuration or strictly match the request origin against the same exact allowlist; never accept an arbitrary request header. Treat page context as untrusted model context, never authorization. Forward the helper response unchanged.',
284
290
  '',
291
+ "Run the generated `test/noodle-assistant.test.ts` with the application's Vitest runner (add the dev dependency using its existing package manager if absent). It tests the real generated route with synthetic identities. Separately exercise the real session function with signed-out, tenant-A, tenant-B and authorized fixtures before claiming integration proof. A green mock suite does not prove customer authorization.",
292
+ '',
285
293
  '`serviceUrl` is the Noodle Seed control-plane base URL: the value `noodle assistant clients create` prints, also stored as `serviceUrl` in `deployment.json`. It is NOT the deployment MCP endpoint (`url`, which ends in `/v1/mcp` and rejects session exchange). Never probe or guess endpoints with real credentials.',
286
294
  '',
287
295
  '### Route customer endpoints from the backend',
@@ -30,7 +30,7 @@ export function renderBuildAnMcpServerReference() {
30
30
  '5. **Author in TypeScript.** Follow `references/authoring-workflow.md` for connector and flow patterns, `references/tool-design.md` for the model-facing tool surface, and `references/sdk-surface.md` for exact builders. These are this route’s complete canonical support set; use the router lookup catalog only when observed evidence names a different concern.',
31
31
  ' MCP protocol versions are platform-owned and negotiated automatically at the serving endpoint. Do not add protocol-version settings to server options, `noodle.json`, app manifests, or deployment configuration.',
32
32
  '6. **Validate and repair.** Run `noodle validate --json`. Parse `error.errors[]`, repair the cited `path`, and rerun validation. Consult the lookup catalog only for the specific reported error code; do not open another reference speculatively.',
33
- '7. **Run the local smoke.** After validation succeeds, run `noodle test --json` and repair any failure at that evidence layer.',
33
+ '7. **Run local behavior.** Start from the generated tests and run `npm test`: compile, tools/list, representative result and invalid-input rejection. `noodle test --json` without `--tool` is registration-only; repair failures before claiming a working capability.',
34
34
  '8. **Prove external behavior.** For connector-backed reads, set credentials through the effective local target and run a safe representative `noodle tools call`. Confirm populated mapped fields from real output, not merely successful registration.',
35
35
  '9. **Stop at the requested boundary.** Do not add an App, host test, hosted environment, publication work, or deployment unless the user requested that outcome. Deploy only when the selected route or the user explicitly requires it.',
36
36
  '',
@@ -40,7 +40,7 @@ export function renderBuildAnMcpServerReference() {
40
40
  '',
41
41
  '- **Authoring:** the requested TypeScript behavior exists with typed inputs and outputs, and the product-guide decision and reason are recorded.',
42
42
  '- **Compilation:** `noodle validate --json` returned success.',
43
- '- **Local smoke:** `noodle test --json` returned success.',
43
+ '- **Local behavior:** the generated/adapted suite passed a representative call and a negative case. Synthetic fixtures prove only their configured behavior.',
44
44
  '- **Connector reality:** a representative safe read via `noodle tools call` returned populated mapped fields. This is required for connector-backed work.',
45
45
  '- **Higher levels:** explicitly report host, deployment, and production checks as not run unless they were separately requested and evidenced.',
46
46
  '',
@@ -27,6 +27,7 @@ export function renderVerifyAndRecoverReference() {
27
27
  '1. **Compile** — the TypeScript build and authoring import surface are valid.',
28
28
  '2. **Validate** — `noodle validate --json` accepts the Noodle contract.',
29
29
  '3. **Local smoke** — `noodle test --json` starts the local runtime. Open apps exercise MCP registration; customer-auth apps must instead pass the anonymous 401 plus exact protected-resource metadata boundary and report `interactiveRequired: true`.',
30
+ ' To prove a safe read, explicitly supply `--tool <read_tool> --args <json>` or use `noodle tools call <read_tool> --args <json> --json`. These commands fail on protocol/tool errors, missing or incomplete results, and a declared output-schema mismatch. `input_required` is not completion: use Devtools for interactive input, never fabricate approval or retry an uncertain write.',
30
31
  '4. **Customer auth** — when customer auth is declared, run `noodle auth doctor src/server.ts --json` for metadata and JWKS readiness, then run `noodle devtools src/server.ts`, complete sign-in, and make one authenticated `tools/list` request or representative safe read. The doctor does not prove that registration or token issuance succeeds; the authenticated request proves issuer, signature, stable audience, and exact-resource binding together.',
31
32
  '5. **Real API** — a representative safe `noodle tools call` proves connector credentials, transport, observed mapping, and populated data.',
32
33
  '6. **App compliance** — `noodle check --json` and local devtools prove the App contract and intended states.',
@@ -57,7 +58,7 @@ export function renderVerifyAndRecoverReference() {
57
58
  '## Recovery paths',
58
59
  '',
59
60
  '- Compile/validation: repair the exact import, schema, or reported path, then rerun that command without freeform changes.',
60
- '- Local boot/smoke: use the structured startup error to correct the effective target, config, or entrypoint before retrying.',
61
+ '- Local boot/smoke: use all reported missing variable and secret names and the exact-target recovery commands together. Values stay outside the diagnostic. `error.detail.reason` identifies incomplete/failed operation evidence; RPC message/data and continuation state are deliberately not copied. A failed response does not prove a write was rolled back.',
61
62
  '- Real API: distinguish authentication, reachability, legitimate empty results, and broken response mappings before changing code.',
62
63
  '- App: repair the cited contract or state in `noodle check --json`, then confirm it in devtools before attempting a host.',
63
64
  '- Host/deployment/production: confirm revision, target, identity, and configuration independently; do not infer one from another.',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodle-borg/agent-kit",
3
- "version": "0.93.0",
3
+ "version": "0.94.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,8 +1,9 @@
1
1
  import { requestModelCompletion } from './model-request.js';
2
2
  const MAX_MODEL_RESPONSE = 1 << 20;
3
+ const MAX_SUGGESTION_TOKENS = 512;
3
4
  /** One bounded, tool-free pass on the active assistant model; malformed output fails closed. */
4
5
  export async function requestAssistantSuggestedPrompts(binding, messages, fetcher, stats, remainingTokens, turnSignal) {
5
- const limit = Math.min(160, remainingTokens ?? binding.requestPolicy?.maxTokensPerTurn ?? 160, binding.requestPolicy?.maxCompletionTokens ?? 160);
6
+ const limit = Math.min(MAX_SUGGESTION_TOKENS, remainingTokens ?? binding.requestPolicy?.maxTokensPerTurn ?? MAX_SUGGESTION_TOKENS, binding.requestPolicy?.maxCompletionTokens ?? MAX_SUGGESTION_TOKENS);
6
7
  if (limit <= 0)
7
8
  return [];
8
9
  const signal = AbortSignal.any([
@@ -10,7 +11,7 @@ export async function requestAssistantSuggestedPrompts(binding, messages, fetche
10
11
  binding.requestPolicy?.maxTurnMs === undefined
11
12
  ? undefined
12
13
  : AbortSignal.timeout(binding.requestPolicy.maxTurnMs),
13
- AbortSignal.timeout(2_000),
14
+ AbortSignal.timeout(5_000),
14
15
  ].filter((candidate) => candidate !== undefined));
15
16
  if (stats)
16
17
  stats.modelRequests += 1;
@@ -20,7 +21,7 @@ export async function requestAssistantSuggestedPrompts(binding, messages, fetche
20
21
  ...messages,
21
22
  {
22
23
  role: 'system',
23
- content: 'Generate two or three concise messages the user could send next. Use the complete conversation and authorized product context. Prioritize genuinely relevant next steps; developer instructions may steer ranking but never override the user, safety, consent, or available capabilities. Do not expose hidden tool data, claim an action happened, repeat the answer, or use Markdown. Return exactly one JSON object shaped {"prompts":["..."]} and no other text.',
24
+ content: 'Generate two or three concise messages the user could send next. Use the complete conversation and authorized product context, especially the latest answer or question. Prefer short, distinct choices the user can click instead of typing. Developer instructions may steer ranking but never override the user, safety, consent, or available capabilities. Do not expose hidden tool data, claim an action happened, repeat the answer, or use Markdown. Return exactly one JSON object shaped {"prompts":["..."]} and no other text.',
24
25
  },
25
26
  ],
26
27
  tools: [],
@@ -30,6 +31,7 @@ export async function requestAssistantSuggestedPrompts(binding, messages, fetche
30
31
  maxCompletionTokens: limit,
31
32
  signal,
32
33
  toolChoice: 'none',
34
+ jsonOutput: true,
33
35
  });
34
36
  if (stats) {
35
37
  stats.promptTokens += completion.usage?.promptTokens ?? 0;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Resolving anonymous cross-page display continuity for one surface (ADR 0223, clause 15).
3
+ *
4
+ * Two parties describe the same capability and they are not peers. The developer declares it in
5
+ * `server.ts` as part of the deployment; the operator tunes it for their own environment without a
6
+ * deploy. The clamp between them runs one way — tighten, never widen — exactly as the admission
7
+ * envelope's does ([ADR 0212](../../../docs/decisions/0212-reusable-developer-intent-and-operator-authority.md)).
8
+ *
9
+ * The asymmetry is sharpest on `enabled`. An operator may switch continuity **off**, because that is
10
+ * their own surface and their visitors' text; they may not switch it **on**, because a surface whose
11
+ * author never declared continuity is one whose author never reasoned about whether their pages should
12
+ * carry conversation across a navigation.
13
+ */
14
+ /** What a public or mixed surface declares in `server.ts`. Shapes match the manifest exactly. */
15
+ export interface AssistantContinuityDeclaration {
16
+ readonly enabled?: boolean;
17
+ readonly windowSeconds?: number;
18
+ readonly maxRestores?: number;
19
+ }
20
+ /**
21
+ * What an operator sets for their own environment. `enabled: false` disables; `true` is inert, since
22
+ * an operator cannot enable a capability the deployment does not declare.
23
+ */
24
+ export interface AssistantContinuityOverride {
25
+ readonly enabled?: boolean;
26
+ readonly windowSeconds?: number;
27
+ readonly maxRestores?: number;
28
+ }
29
+ export interface ResolvedAssistantContinuity {
30
+ readonly enabled: boolean;
31
+ /** Zero whenever `enabled` is false, so a caller cannot read a live window off a dead surface. */
32
+ readonly windowMs: number;
33
+ readonly maxRestores: number;
34
+ }
35
+ export declare function resolveContinuity(declaration: AssistantContinuityDeclaration | undefined, override: AssistantContinuityOverride | undefined): ResolvedAssistantContinuity;
36
+ //# sourceMappingURL=continuity-bounds.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { ASSISTANT_CONTINUITY_MAX_RESTORES, ASSISTANT_CONTINUITY_RESTORE_CEILING, ASSISTANT_CONTINUITY_WINDOW_CEILING_MS, ASSISTANT_CONTINUITY_WINDOW_MS, continuityMaxRestores, continuityWindowMs, } from './continuity-store.js';
2
+ const OFF = { enabled: false, windowMs: 0, maxRestores: 0 };
3
+ /**
4
+ * A declared bound that is not a non-negative integer fails the surface closed rather than falling back
5
+ * to the shipped default.
6
+ *
7
+ * This is deliberately the opposite of the spend ladder's malformed-allowance rule, which fails to
8
+ * *absent* because an outage costs more than a day of unbudgeted spend. The thing configured here is a
9
+ * capability, and the safe failure for a capability is not to exist: a typo that silently grants a
10
+ * 300-second window nobody asked for is worse than a typo that grants nothing.
11
+ */
12
+ function boundedInteger(value) {
13
+ if (value === undefined)
14
+ return undefined;
15
+ return Number.isInteger(value) && value >= 0 ? value : 'invalid';
16
+ }
17
+ export function resolveContinuity(declaration, override) {
18
+ if (declaration?.enabled !== true)
19
+ return OFF;
20
+ if (override?.enabled === false)
21
+ return OFF;
22
+ const declaredWindow = boundedInteger(declaration.windowSeconds);
23
+ const declaredRestores = boundedInteger(declaration.maxRestores);
24
+ const overriddenWindow = boundedInteger(override?.windowSeconds);
25
+ const overriddenRestores = boundedInteger(override?.maxRestores);
26
+ if (declaredWindow === 'invalid' ||
27
+ declaredRestores === 'invalid' ||
28
+ overriddenWindow === 'invalid' ||
29
+ overriddenRestores === 'invalid') {
30
+ return OFF;
31
+ }
32
+ // Each side is clamped to the structural ceiling first, then the tighter of the two wins. Clamping
33
+ // before the `Math.min` means an over-eager operator value degrades to the ceiling rather than
34
+ // becoming one, which is the same shape `surfaceEnvelope` uses for daily budgets.
35
+ const windowMs = Math.min(declaredWindow === undefined
36
+ ? ASSISTANT_CONTINUITY_WINDOW_MS
37
+ : continuityWindowMs(declaredWindow * 1000), overriddenWindow === undefined
38
+ ? ASSISTANT_CONTINUITY_WINDOW_CEILING_MS
39
+ : continuityWindowMs(overriddenWindow * 1000));
40
+ const maxRestores = Math.min(declaredRestores === undefined
41
+ ? ASSISTANT_CONTINUITY_MAX_RESTORES
42
+ : continuityMaxRestores(declaredRestores), overriddenRestores === undefined
43
+ ? ASSISTANT_CONTINUITY_RESTORE_CEILING
44
+ : continuityMaxRestores(overriddenRestores));
45
+ // Zero from either party is a real value and is the kill switch, so it must not read as "unset".
46
+ // Collapsing it to `OFF` here keeps every caller from having to remember that a zero-length window
47
+ // and a disabled surface are the same thing.
48
+ if (windowMs === 0 || maxRestores === 0)
49
+ return OFF;
50
+ return { enabled: true, windowMs, maxRestores };
51
+ }
52
+ //# sourceMappingURL=continuity-bounds.js.map
@@ -0,0 +1,115 @@
1
+ import type { TenantRef } from './tenant-ref.js';
2
+ /**
3
+ * Anonymous cross-page display continuity (ADR 0223, clauses 11-16).
4
+ *
5
+ * **A display handle, not a session.** Claiming one returns the conversation's visible text for
6
+ * rendering and mints a fresh session; it never revives the original, and grants no tool authority, no
7
+ * share of a spent turn budget, and no bound surface. Deliberately unlike the authenticated reattach in
8
+ * clause 7, it restores no view descriptor and no pending interaction — a capability that survives a
9
+ * navigation is exactly what this design refuses to create.
10
+ *
11
+ * **Its bounds are shaped by one asymmetry.** A sign-in ticket is safe partly because possession alone
12
+ * is worthless: spending one also requires the customer's client credentials, held server-side. An
13
+ * anonymous visitor has no such credential and no server-side counterpart, so possession of a handle is
14
+ * *sufficient by itself*. Hence four independent bounds — single use, a short window, binding to the
15
+ * context it was issued to, and a chain that ends — rather than any one of them carrying the weight.
16
+ */
17
+ /** What a handle is bound to. All three are hashes; no raw origin or visitor id is ever stored. */
18
+ export interface AssistantContinuityContext {
19
+ readonly embedId: string;
20
+ readonly originHash: string;
21
+ readonly visitorHash: string;
22
+ }
23
+ export interface AssistantContinuityRecord {
24
+ readonly id: string;
25
+ readonly sessionId: string;
26
+ readonly tenant: TenantRef;
27
+ readonly context: AssistantContinuityContext;
28
+ readonly handleHash: string;
29
+ /** Restores already spent on this conversation, carried across every rotation. */
30
+ readonly restoreCount: number;
31
+ /** Clamped at issue time and stored, so a record describes its own limit rather than trusting a caller. */
32
+ readonly maxRestores: number;
33
+ readonly createdAt: string;
34
+ readonly expiresAt: string;
35
+ readonly claimedAt?: string;
36
+ }
37
+ export type AssistantContinuityClaim = {
38
+ readonly ok: true;
39
+ readonly record: AssistantContinuityRecord;
40
+ /**
41
+ * The rotated handle for the next navigation, or `undefined` once the chain is spent. Withholding
42
+ * it is how continuity ends: the visitor still sees the text they were promised, and the next page
43
+ * simply has nothing to present, so no refusal code is needed for an ordinary ending.
44
+ */
45
+ readonly handle?: string;
46
+ } | {
47
+ readonly ok: false;
48
+ /**
49
+ * One code per refusal, because they are operationally different: `unknown` is a bad or already
50
+ * spent handle, `expired` is a visitor who took too long, and `context_mismatch` is a handle
51
+ * presented from an embed, origin, or visitor it was not issued to — the exfiltration signal, and
52
+ * the one worth alerting on, so it must not collapse into the first.
53
+ */
54
+ readonly reason: 'unknown' | 'expired' | 'context_mismatch';
55
+ };
56
+ /** Five minutes: long enough to read a page before clicking through, short enough to bound a leak. */
57
+ export declare const ASSISTANT_CONTINUITY_WINDOW_MS: number;
58
+ /**
59
+ * Ten minutes, the sign-in ticket's own TTL: an anonymous display handle must never outlive the
60
+ * authenticated capability it is modelled on, whatever a developer or operator asks for.
61
+ */
62
+ export declare const ASSISTANT_CONTINUITY_WINDOW_CEILING_MS: number;
63
+ /** Three restores covers the engaged visitor's page depth without letting a chain run indefinitely. */
64
+ export declare const ASSISTANT_CONTINUITY_MAX_RESTORES = 3;
65
+ /** Past ten, chain length stops being a meaningful control, so no configuration may exceed it. */
66
+ export declare const ASSISTANT_CONTINUITY_RESTORE_CEILING = 10;
67
+ export declare function continuityWindowMs(requested?: number): number;
68
+ export declare function continuityMaxRestores(requested?: number): number;
69
+ export interface AssistantContinuityStore {
70
+ /**
71
+ * Issue this session's handle for the next navigation, superseding any unclaimed one so a
72
+ * conversation never accumulates one live key per turn.
73
+ *
74
+ * Returns `undefined` when the effective window or chain clamps to zero. The kill switch is enforced
75
+ * here rather than at the call site, so a caller that forgets to check cannot hand out a handle the
76
+ * operator has switched off.
77
+ */
78
+ issue(input: {
79
+ readonly sessionId: string;
80
+ readonly tenant: TenantRef;
81
+ readonly context: AssistantContinuityContext;
82
+ readonly windowMs?: number;
83
+ readonly maxRestores?: number;
84
+ readonly now: Date;
85
+ }): Promise<{
86
+ readonly record: AssistantContinuityRecord;
87
+ readonly handle: string;
88
+ } | undefined>;
89
+ /**
90
+ * Spend a handle exactly once, rotating to a fresh one until the chain is spent. Atomic: two backends
91
+ * racing the same value must not both win, which is why this is one store seam rather than a
92
+ * read-then-write in the route.
93
+ */
94
+ claim(input: {
95
+ readonly handle: string;
96
+ readonly context: AssistantContinuityContext;
97
+ readonly now: Date;
98
+ }): Promise<AssistantContinuityClaim>;
99
+ /**
100
+ * Drop spent and expired rows, returning how many went.
101
+ *
102
+ * Retention belongs to this store rather than to a caller that may forget: handles are short-lived and
103
+ * must be swept, not accumulated. Cleanup may lag expiry safely, because `claim` already refuses a
104
+ * stale handle — the sweep bounds the table, it does not enforce the window.
105
+ */
106
+ sweepExpired(input: {
107
+ readonly now: Date;
108
+ }): Promise<number>;
109
+ }
110
+ export declare function continuityHandle(): string;
111
+ export declare function continuityDigest(value: string): string;
112
+ export declare function continuityHashesEqual(left: string, right: string): boolean;
113
+ /** Every leg must match: a handle is only valid for the embed, origin, and visitor it was issued to. */
114
+ export declare function sameContinuityContext(left: AssistantContinuityContext, right: AssistantContinuityContext): boolean;
115
+ //# sourceMappingURL=continuity-store.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ /** Five minutes: long enough to read a page before clicking through, short enough to bound a leak. */
3
+ export const ASSISTANT_CONTINUITY_WINDOW_MS = 5 * 60 * 1000;
4
+ /**
5
+ * Ten minutes, the sign-in ticket's own TTL: an anonymous display handle must never outlive the
6
+ * authenticated capability it is modelled on, whatever a developer or operator asks for.
7
+ */
8
+ export const ASSISTANT_CONTINUITY_WINDOW_CEILING_MS = 10 * 60 * 1000;
9
+ /** Three restores covers the engaged visitor's page depth without letting a chain run indefinitely. */
10
+ export const ASSISTANT_CONTINUITY_MAX_RESTORES = 3;
11
+ /** Past ten, chain length stops being a meaningful control, so no configuration may exceed it. */
12
+ export const ASSISTANT_CONTINUITY_RESTORE_CEILING = 10;
13
+ /**
14
+ * Clamp a requested bound the way the admission envelope does: one direction only.
15
+ *
16
+ * Zero is the deploy-free kill switch, and anything malformed — negative, fractional, NaN — collapses to
17
+ * zero rather than to the default. A typo must fail closed here: unlike a spend allowance, where an
18
+ * outage is worse than a day of unbudgeted cost, a continuity handle is a capability, and the safe
19
+ * failure for a capability is not to exist.
20
+ */
21
+ function clampBound(requested, fallback, ceiling) {
22
+ if (requested === undefined)
23
+ return fallback;
24
+ if (!Number.isInteger(requested) || requested < 0)
25
+ return 0;
26
+ return Math.min(requested, ceiling);
27
+ }
28
+ export function continuityWindowMs(requested) {
29
+ return clampBound(requested, ASSISTANT_CONTINUITY_WINDOW_MS, ASSISTANT_CONTINUITY_WINDOW_CEILING_MS);
30
+ }
31
+ export function continuityMaxRestores(requested) {
32
+ return clampBound(requested, ASSISTANT_CONTINUITY_MAX_RESTORES, ASSISTANT_CONTINUITY_RESTORE_CEILING);
33
+ }
34
+ export function continuityHandle() {
35
+ return `cnt_${randomBytes(24).toString('base64url')}`;
36
+ }
37
+ export function continuityDigest(value) {
38
+ return createHash('sha256').update(value).digest('hex');
39
+ }
40
+ export function continuityHashesEqual(left, right) {
41
+ const a = Buffer.from(left, 'utf8');
42
+ const b = Buffer.from(right, 'utf8');
43
+ return a.length === b.length && timingSafeEqual(a, b);
44
+ }
45
+ /** Every leg must match: a handle is only valid for the embed, origin, and visitor it was issued to. */
46
+ export function sameContinuityContext(left, right) {
47
+ return (left.embedId === right.embedId &&
48
+ left.originHash === right.originHash &&
49
+ left.visitorHash === right.visitorHash);
50
+ }
51
+ //# sourceMappingURL=continuity-store.js.map
@@ -0,0 +1,26 @@
1
+ import { type AssistantContinuityClaim, type AssistantContinuityContext, type AssistantContinuityRecord, type AssistantContinuityStore } from './continuity-store.js';
2
+ import type { TenantRef } from './tenant-ref.js';
3
+ /** The in-memory half of the pair. Both run the shared parity suite; neither is the only proof. */
4
+ export declare class InMemoryAssistantContinuityStore implements AssistantContinuityStore {
5
+ #private;
6
+ issue(input: {
7
+ readonly sessionId: string;
8
+ readonly tenant: TenantRef;
9
+ readonly context: AssistantContinuityContext;
10
+ readonly windowMs?: number;
11
+ readonly maxRestores?: number;
12
+ readonly now: Date;
13
+ }): Promise<{
14
+ readonly record: AssistantContinuityRecord;
15
+ readonly handle: string;
16
+ } | undefined>;
17
+ claim(input: {
18
+ readonly handle: string;
19
+ readonly context: AssistantContinuityContext;
20
+ readonly now: Date;
21
+ }): Promise<AssistantContinuityClaim>;
22
+ sweepExpired(input: {
23
+ readonly now: Date;
24
+ }): Promise<number>;
25
+ }
26
+ //# sourceMappingURL=in-memory-continuity-store.d.ts.map