@noodleseed/one 0.132.2 → 0.134.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 (102) hide show
  1. package/dist/commands/author-loop.d.ts.map +1 -1
  2. package/dist/commands/author-loop.js +3 -3
  3. package/dist/commands/author-loop.js.map +1 -1
  4. package/dist/commands/devtools.js +1 -1
  5. package/dist/commands/devtools.js.map +1 -1
  6. package/dist/commands/local-readiness.d.ts +2 -2
  7. package/dist/commands/local-readiness.d.ts.map +1 -1
  8. package/dist/commands/local-readiness.js +15 -7
  9. package/dist/commands/local-readiness.js.map +1 -1
  10. package/dist/dev-local-runtime.d.ts +33 -0
  11. package/dist/dev-local-runtime.d.ts.map +1 -0
  12. package/dist/dev-local-runtime.js +53 -0
  13. package/dist/dev-local-runtime.js.map +1 -0
  14. package/dist/dev.d.ts +4 -0
  15. package/dist/dev.d.ts.map +1 -1
  16. package/dist/dev.js +109 -31
  17. package/dist/dev.js.map +1 -1
  18. package/dist/devtools-harness.js +5 -3
  19. package/dist/devtools-harness.js.map +1 -1
  20. package/dist/devtools-preview.d.ts +2 -0
  21. package/dist/devtools-preview.d.ts.map +1 -1
  22. package/dist/devtools-preview.js +15 -3
  23. package/dist/devtools-preview.js.map +1 -1
  24. package/dist/devtools-rpc-forwarder.js +1 -1
  25. package/dist/devtools-rpc-forwarder.js.map +1 -1
  26. package/dist/preview-session.d.ts.map +1 -1
  27. package/dist/preview-session.js +1 -0
  28. package/dist/preview-session.js.map +1 -1
  29. package/dist/react-widget-build.d.ts +4 -0
  30. package/dist/react-widget-build.d.ts.map +1 -1
  31. package/dist/react-widget-build.js +9 -0
  32. package/dist/react-widget-build.js.map +1 -1
  33. package/node_modules/@noodle-borg/admission-limits/dist/client-address.d.ts +7 -0
  34. package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +71 -0
  35. package/node_modules/@noodle-borg/admission-limits/dist/envelope.d.ts +67 -0
  36. package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +15 -0
  37. package/node_modules/@noodle-borg/admission-limits/dist/portable.d.ts +6 -0
  38. package/node_modules/@noodle-borg/admission-limits/dist/portable.js +6 -0
  39. package/node_modules/@noodle-borg/admission-limits/package.json +8 -0
  40. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +6 -6
  41. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +1 -1
  42. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +4 -1
  43. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/artifact-projection.d.ts +21 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-context.d.ts +25 -0
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-customer-issuer.d.ts +3 -0
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-customer-routing.d.ts +19 -0
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +39 -0
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interaction-state.d.ts +181 -0
  50. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.d.ts +99 -0
  51. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-interactive.js +1 -1
  52. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-presentation.d.ts +35 -0
  53. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-sensitive-values.d.ts +3 -0
  54. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +226 -0
  55. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +1 -1
  56. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation-store.d.ts +70 -0
  57. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.d.ts +68 -0
  58. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-script.d.ts +24 -0
  59. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +73 -0
  60. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-elevation-store.d.ts +21 -0
  61. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.d.ts +28 -0
  62. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +24 -0
  63. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +56 -0
  64. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +21 -0
  65. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +2 -1
  66. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.d.ts +44 -0
  67. package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.d.ts +22 -0
  68. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +16 -0
  69. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +1 -1
  70. package/node_modules/@noodle-borg/assistant-gateway/dist/tenant-ref.d.ts +13 -0
  71. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +9 -2
  72. package/node_modules/@noodle-borg/authoring/dist/assistant.js +8 -2
  73. package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +3 -1
  74. package/node_modules/@noodle-borg/authoring/dist/connectors.js +5 -1
  75. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +2 -1
  76. package/node_modules/@noodle-borg/authoring/dist/server.js +4 -1
  77. package/node_modules/@noodle-borg/compiler/dist/artifact/assemble.js +1 -1
  78. package/node_modules/@noodle-borg/compiler/dist/artifact/config.js +25 -20
  79. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +16 -12
  80. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +18 -13
  81. package/node_modules/@noodle-borg/connector-defs/dist/compile-http.js +2 -0
  82. package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +3 -3
  83. package/node_modules/@noodle-borg/connector-defs/dist/schema.js +6 -1
  84. package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +31 -8
  85. package/node_modules/@noodle-borg/protocol/dist/mapping.js +8 -0
  86. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -0
  87. package/node_modules/@noodle-borg/runtime/dist/index.js +1 -0
  88. package/node_modules/@noodle-borg/runtime/dist/managed-origins.d.ts +11 -0
  89. package/node_modules/@noodle-borg/runtime/dist/managed-origins.js +105 -0
  90. package/node_modules/@noodle-borg/service/dist/local-options.d.ts +9 -0
  91. package/node_modules/@noodle-borg/service/dist/registry-compile.js +12 -12
  92. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +14 -3
  93. package/node_modules/@noodle-borg/service/dist/routes/assistant-embeds.js +1 -1
  94. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +1 -1
  95. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-turn.js +1 -1
  96. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +1 -1
  97. package/node_modules/@noodle-borg/service/dist/serve-local.js +2 -3
  98. package/node_modules/@noodle-borg/service/package.json +1 -1
  99. package/node_modules/@noodleseed/assistant/README.md +4 -1
  100. package/package.json +5 -3
  101. package/node_modules/@noodle-borg/admission-limits/dist/index.js +0 -15
  102. package/node_modules/@noodle-borg/admission-limits/dist/postgres-counter-store.js +0 -68
@@ -0,0 +1,226 @@
1
+ import type { ArtifactServer } from '@noodle-borg/compiler';
2
+ import type { CallerIdentity } from '@noodle-borg/runtime';
3
+ import type { AssistantContextPreferences } from './assistant-context.js';
4
+ import type { AssistantCustomerRouting } from './assistant-customer-routing.js';
5
+ import { type AssistantInteractionClaimResult, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord } from './assistant-interaction-state.js';
6
+ import type { TenantRef } from './tenant-ref.js';
7
+ export { ASSISTANT_INTERACTION_EXECUTING_GRACE_MS, ASSISTANT_INTERACTION_OUTCOME_RETENTION_MS, type AssistantConfirmationInteractionRecord, AssistantInteractionCapacityError, type AssistantInteractionClaimResult, type AssistantInteractionCompletion, type AssistantInteractionCompletionResult, type AssistantInteractionCreateInput, type AssistantInteractionKind, type AssistantInteractionPublicArray, type AssistantInteractionPublicObject, type AssistantInteractionPublicOutcome, type AssistantInteractionPublicValue, type AssistantInteractionRecord, type AssistantInteractionScope, type AssistantInteractionStatus, type AssistantInteractionTransitionInput, type AssistantInteractionTransitionNext, type AssistantInteractionTransitionResult, type AssistantPendingConfirmationInteractionRecord, type AssistantPendingInputInteractionRecord, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, } from './assistant-interaction-state.js';
8
+ export declare const ASSISTANT_SESSION_IDLE_MS: number;
9
+ /**
10
+ * The only `server.assistant` fields that may reach a browser.
11
+ *
12
+ * It lives here, beside the record it types, so the allowlist and the shape it produces cannot drift
13
+ * apart: the projection in `packages/service` iterates this same list. A denylist here would be a type
14
+ * that lies — it once admitted `sessionClaims` key names, and would now admit the public surface's
15
+ * capability allowlist — so growth in the manifest must not be able to grow the browser payload.
16
+ */
17
+ export declare const ASSISTANT_BROWSER_UI_FIELDS: readonly ["layout", "behavior", "labels", "presentation", "suggestedPrompts", "privacyUrl", "termsUrl", "locale", "direction"];
18
+ export interface AssistantClientRecord {
19
+ readonly id: string;
20
+ readonly name: string;
21
+ readonly tenant: TenantRef;
22
+ readonly deploymentId: string;
23
+ readonly allowedOrigins: readonly string[];
24
+ readonly secretHash: string;
25
+ readonly createdAt: string;
26
+ readonly revokedAt?: string;
27
+ }
28
+ export interface AssistantSessionRecord {
29
+ readonly id: string;
30
+ readonly tokenHash: string;
31
+ readonly clientId: string;
32
+ readonly tenant: TenantRef;
33
+ readonly deploymentId: string;
34
+ readonly origin: string;
35
+ readonly caller: CallerIdentity;
36
+ /** Canonical customer endpoint routes; private session authority, never caller or response data. */
37
+ readonly customerRouting?: AssistantCustomerRouting;
38
+ readonly context?: Readonly<Record<string, string | number | boolean | null>>;
39
+ readonly preferences?: AssistantContextPreferences;
40
+ readonly configuration?: {
41
+ readonly branding?: ArtifactServer['branding'];
42
+ readonly assistant?: Pick<NonNullable<ArtifactServer['assistant']>, (typeof ASSISTANT_BROWSER_UI_FIELDS)[number]>;
43
+ };
44
+ /**
45
+ * The public surface this session was minted from, when it was minted anonymously from a page rather
46
+ * than exchanged through a customer backend. It is the key admission spends against, so it stays on
47
+ * the record for the session's whole life — including after a mixed surface elevates the caller, whose
48
+ * turns still belong to the surface that admitted them.
49
+ */
50
+ readonly publicEmbedId?: string;
51
+ /**
52
+ * The intercepted tool a successful elevation may re-attempt as the session's first elevated
53
+ * turn. Armed only in the same statement that elevates; consumed (or mooted by the visitor's
54
+ * first typed turn) exactly once via {@link AssistantStore.consumePendingResume}.
55
+ */
56
+ readonly pendingResume?: AssistantPendingResume;
57
+ readonly createdAt: string;
58
+ readonly expiresAt: string;
59
+ readonly absoluteExpiresAt: string;
60
+ readonly history: AssistantHistoryMessage[];
61
+ /**
62
+ * Model turns already spent. Durable and separate from `history`, which truncates to the last twelve
63
+ * messages: history is a prompt-window trim, never a bound.
64
+ */
65
+ readonly turnCount: number;
66
+ }
67
+ /** The intercepted call a spent sign-in ticket left pending, resumable at most once. */
68
+ export interface AssistantPendingResume {
69
+ readonly tool: string;
70
+ readonly requestedAt: string;
71
+ }
72
+ /** One consumed turn slot, or the refusal that the session has spent its allowance. */
73
+ export interface AssistantTurnConsumption {
74
+ readonly allowed: boolean;
75
+ readonly turnCount: number;
76
+ }
77
+ export interface AssistantHistoryMessage {
78
+ readonly role: 'user' | 'assistant';
79
+ readonly content: string;
80
+ }
81
+ export type AssistantSessionElevation = {
82
+ readonly ok: true;
83
+ readonly session: AssistantSessionRecord;
84
+ /** The replacement token. The value it replaces is dead the moment this returns. */
85
+ readonly token: string;
86
+ } | {
87
+ readonly ok: false;
88
+ readonly reason: 'unknown_session' | 'already_elevated';
89
+ };
90
+ export interface AssistantStore {
91
+ createClient(input: {
92
+ readonly name: string;
93
+ readonly tenant: TenantRef;
94
+ readonly deploymentId: string;
95
+ readonly allowedOrigins: readonly string[];
96
+ readonly now: Date;
97
+ }): Promise<{
98
+ readonly client: AssistantClientRecord;
99
+ readonly secret: string;
100
+ }>;
101
+ listClients(tenant: TenantRef): Promise<readonly AssistantClientRecord[]>;
102
+ rotateClient(id: string, now: Date): Promise<{
103
+ readonly client: AssistantClientRecord;
104
+ readonly secret: string;
105
+ } | undefined>;
106
+ revokeClient(id: string, now: Date): Promise<boolean>;
107
+ authenticateClient(id: string, secret: string): Promise<AssistantClientRecord | undefined>;
108
+ createSession(input: Omit<AssistantSessionRecord, 'id' | 'tokenHash' | 'history' | 'turnCount'>): Promise<{
109
+ readonly session: AssistantSessionRecord;
110
+ readonly token: string;
111
+ }>;
112
+ /**
113
+ * Spend one turn if the session has one left, atomically. This is one seam rather than a
114
+ * read-then-write in the route because two turns arriving together at the last slot is precisely the
115
+ * race a read-then-write loses. A refused turn does not advance the count.
116
+ */
117
+ consumeTurn(id: string, limit: number): Promise<AssistantTurnConsumption>;
118
+ /**
119
+ * Bind an already-open conversation to a signed-in caller (ADR 0201, 5.6b).
120
+ *
121
+ * One seam, and one row: the session is **mutated in place** rather than replaced, so the visitor
122
+ * keeps the conversation they were having. The old token dies with the same statement that installs
123
+ * the new one, which is why this is not a read-then-write — an elevation that left the anonymous
124
+ * token briefly alive would be a window in which both identities could act.
125
+ *
126
+ * `publicEmbedId` is deliberately retained: admission keeps charging the surface that admitted the
127
+ * visitor, exactly as 5.4 designed when it chose to key sessions on the embed rather than on being
128
+ * anonymous. Refuses when the session is already non-anonymous, which is what makes a second
129
+ * elevation impossible without a separate check to forget.
130
+ */
131
+ elevateSession(input: {
132
+ readonly sessionId: string;
133
+ readonly caller: AssistantSessionRecord['caller'];
134
+ /** The elevating client: becomes the session's delegated-credential issuer basis (ADR 0152). */
135
+ readonly clientId: string;
136
+ /** Allowlist-validated by the route: becomes the session's CORS pin, where the conversation continues. */
137
+ readonly origin: string;
138
+ /** Backend-verified customer routes; absent means unchanged, never cleared. */
139
+ readonly customerRouting?: AssistantSessionRecord['customerRouting'];
140
+ /** Arms the one-shot resume of the intercepted tool, in the same statement that elevates. */
141
+ readonly pendingResume?: AssistantPendingResume;
142
+ readonly now: Date;
143
+ }): Promise<AssistantSessionElevation>;
144
+ /**
145
+ * Clear and return the pending resume exactly once, atomically — the turn route calls this both
146
+ * to run the resume and to moot it when the visitor types first, and two requests racing the
147
+ * same arm must not both see it.
148
+ */
149
+ consumePendingResume(sessionId: string): Promise<AssistantPendingResume | undefined>;
150
+ getSession(token: string, now: Date): Promise<AssistantSessionRecord | undefined>;
151
+ appendHistory(id: string, messages: readonly AssistantHistoryMessage[]): Promise<void>;
152
+ createInteraction(input: Extract<AssistantInteractionCreateInput, {
153
+ readonly kind: 'confirmation';
154
+ }>): Promise<AssistantPendingConfirmationInteractionRecord>;
155
+ createInteraction(input: Extract<AssistantInteractionCreateInput, {
156
+ readonly kind: 'input';
157
+ }>): Promise<AssistantPendingInputInteractionRecord>;
158
+ claimInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionClaimResult>;
159
+ completeInteraction(input: AssistantInteractionScope & {
160
+ readonly completion: import('./assistant-interaction-state.js').AssistantInteractionCompletion;
161
+ }): Promise<AssistantInteractionCompletionResult>;
162
+ transitionInteraction(input: AssistantInteractionTransitionInput): Promise<AssistantInteractionTransitionResult>;
163
+ getInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionRecord | undefined>;
164
+ /** @deprecated Temporary compatibility for the accept-only route while it moves to claim/complete. */
165
+ consumeInteraction(id: string, sessionId: string, deploymentId: string, now: Date): Promise<(import('./assistant-interaction-state.js').AssistantConfirmationInteractionRecord & {
166
+ readonly status: 'executing';
167
+ }) | undefined>;
168
+ consumeConsoleApprovalNonce(nonce: string, subject: string, expiresAt: Date, now: Date): Promise<boolean>;
169
+ }
170
+ export declare class InMemoryAssistantStore implements AssistantStore {
171
+ #private;
172
+ constructor(options?: {
173
+ readonly maxPendingInteractionsPerSession?: number;
174
+ });
175
+ createClient(input: {
176
+ readonly name: string;
177
+ readonly tenant: TenantRef;
178
+ readonly deploymentId: string;
179
+ readonly allowedOrigins: readonly string[];
180
+ readonly now: Date;
181
+ }): Promise<{
182
+ readonly client: AssistantClientRecord;
183
+ readonly secret: string;
184
+ }>;
185
+ listClients(tenant: TenantRef): Promise<readonly AssistantClientRecord[]>;
186
+ rotateClient(id: string, now: Date): Promise<{
187
+ readonly client: AssistantClientRecord;
188
+ readonly secret: string;
189
+ } | undefined>;
190
+ revokeClient(id: string, now: Date): Promise<boolean>;
191
+ authenticateClient(id: string, secret: string): Promise<AssistantClientRecord | undefined>;
192
+ createSession(input: Omit<AssistantSessionRecord, 'id' | 'tokenHash' | 'history' | 'turnCount'>): Promise<{
193
+ readonly session: AssistantSessionRecord;
194
+ readonly token: string;
195
+ }>;
196
+ elevateSession(input: {
197
+ readonly sessionId: string;
198
+ readonly caller: AssistantSessionRecord['caller'];
199
+ readonly clientId: string;
200
+ readonly origin: string;
201
+ readonly customerRouting?: AssistantSessionRecord['customerRouting'];
202
+ readonly pendingResume?: AssistantPendingResume;
203
+ readonly now: Date;
204
+ }): Promise<AssistantSessionElevation>;
205
+ consumePendingResume(sessionId: string): Promise<AssistantPendingResume | undefined>;
206
+ consumeTurn(id: string, limit: number): Promise<AssistantTurnConsumption>;
207
+ getSession(token: string, now: Date): Promise<AssistantSessionRecord | undefined>;
208
+ appendHistory(id: string, messages: readonly AssistantHistoryMessage[]): Promise<void>;
209
+ createInteraction(input: Extract<AssistantInteractionCreateInput, {
210
+ readonly kind: 'confirmation';
211
+ }>): Promise<AssistantPendingConfirmationInteractionRecord>;
212
+ createInteraction(input: Extract<AssistantInteractionCreateInput, {
213
+ readonly kind: 'input';
214
+ }>): Promise<AssistantPendingInputInteractionRecord>;
215
+ claimInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionClaimResult>;
216
+ completeInteraction(input: AssistantInteractionScope & {
217
+ readonly completion: import('./assistant-interaction-state.js').AssistantInteractionCompletion;
218
+ }): Promise<AssistantInteractionCompletionResult>;
219
+ transitionInteraction(input: AssistantInteractionTransitionInput): Promise<AssistantInteractionTransitionResult>;
220
+ getInteraction(input: AssistantInteractionScope): Promise<AssistantInteractionRecord | undefined>;
221
+ consumeInteraction(id: string, sessionId: string, deploymentId: string, now: Date): Promise<(import('./assistant-interaction-state.js').AssistantConfirmationInteractionRecord & {
222
+ readonly status: 'executing';
223
+ }) | undefined>;
224
+ consumeConsoleApprovalNonce(nonce: string, subject: string, expiresAt: Date, now: Date): Promise<boolean>;
225
+ }
226
+ //# sourceMappingURL=assistant-store.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
- import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits';
2
+ import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
3
3
  import { AssistantInteractionCapacityError, cloneInteraction, completedInteraction, createPendingInteraction, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, executingInteraction, expireStrandedExecutingInteraction, isPrunableInteraction, isTerminalInteraction, shouldExpireStrandedExecutingInteraction, transitionedInteractions, } from './assistant-interaction-state.js';
4
4
  export { ASSISTANT_INTERACTION_EXECUTING_GRACE_MS, ASSISTANT_INTERACTION_OUTCOME_RETENTION_MS, AssistantInteractionCapacityError, DEFAULT_MAX_PENDING_INTERACTIONS_PER_SESSION, } from './assistant-interaction-state.js';
5
5
  export const ASSISTANT_SESSION_IDLE_MS = 30 * 60 * 1000;
@@ -0,0 +1,70 @@
1
+ import type { TenantRef } from './tenant-ref.js';
2
+ /**
3
+ * Mid-conversation sign-in on a `mixed` surface (ADR 0201, 5.6b).
4
+ *
5
+ * **Not an interaction record, deliberately.** The plan proposed a new `auth_requested` kind on the
6
+ * existing interaction machinery, but that machinery is *visitor-resolvable*: accept/decline/cancel are
7
+ * driven from the widget. An elevation resolved by a visitor click would be an elevation with no
8
+ * credentials, so reusing it would have meant bolting a "this kind is not visitor-resolvable" exception
9
+ * onto every resolution path and hoping none was missed. A separate record has no such path to forget.
10
+ *
11
+ * **The continuation is a capability that travels through the browser**, so possession alone must be
12
+ * worthless: spending one also requires the customer's client credentials, and the claim checks that the
13
+ * client's tenant owns the session. It is stored hashed for the same reason session tokens are — the
14
+ * store never holds a value that could be replayed if it leaked.
15
+ */
16
+ export interface AssistantElevationRecord {
17
+ readonly id: string;
18
+ readonly sessionId: string;
19
+ readonly tenant: TenantRef;
20
+ /** The capability the visitor was refused, carried so the audit says what they signed in for. */
21
+ readonly tool: string;
22
+ readonly continuationHash: string;
23
+ readonly createdAt: string;
24
+ readonly expiresAt: string;
25
+ readonly claimedAt?: string;
26
+ }
27
+ export type AssistantElevationClaim = {
28
+ readonly ok: true;
29
+ readonly elevation: AssistantElevationRecord;
30
+ } | {
31
+ readonly ok: false;
32
+ /**
33
+ * One code per refusal, because they are operationally different: `unknown` is a bad or already
34
+ * spent continuation, `expired` is a visitor who took too long, and `tenant_mismatch` is a client
35
+ * spending a continuation for a conversation it does not own — the last is the one worth alerting
36
+ * on, so it must not collapse into the first.
37
+ */
38
+ readonly reason: 'unknown' | 'expired' | 'tenant_mismatch';
39
+ };
40
+ /** Ten minutes: long enough for a real login round trip, short enough that a leaked value is stale. */
41
+ export declare const ASSISTANT_ELEVATION_TTL_MS: number;
42
+ export interface AssistantElevationStore {
43
+ /**
44
+ * Open one elevation for a session. At most one may be live at a time — a second request supersedes
45
+ * the first rather than accumulating, so a model that asks twice cannot mint unbounded capabilities.
46
+ */
47
+ request(input: {
48
+ readonly sessionId: string;
49
+ readonly tenant: TenantRef;
50
+ readonly tool: string;
51
+ readonly now: Date;
52
+ }): Promise<{
53
+ readonly elevation: AssistantElevationRecord;
54
+ readonly continuation: string;
55
+ }>;
56
+ /**
57
+ * Spend a continuation exactly once. Atomic: two backends racing the same value must not both win,
58
+ * which is why this is one store seam rather than a read-then-write in the route.
59
+ */
60
+ claim(input: {
61
+ readonly continuation: string;
62
+ readonly tenant: TenantRef;
63
+ readonly now: Date;
64
+ }): Promise<AssistantElevationClaim>;
65
+ }
66
+ export declare function elevationContinuation(): string;
67
+ export declare function elevationDigest(value: string): string;
68
+ export declare function elevationHashesEqual(left: string, right: string): boolean;
69
+ export declare function sameTenant(left: TenantRef, right: TenantRef): boolean;
70
+ //# sourceMappingURL=elevation-store.d.ts.map
@@ -0,0 +1,68 @@
1
+ import type { AssistantPendingResume, AssistantSessionRecord } from './assistant-store.js';
2
+ import type { AssistantElevationStore } from './elevation-store.js';
3
+ import type { TenantRef } from './tenant-ref.js';
4
+ /**
5
+ * The elevation decision, with no HTTP in it.
6
+ *
7
+ * Kept transport-free for the same reason `mintPublicSession` is: the interesting part is an ordering of
8
+ * refusals, and an ordering is only testable without standing up a server. `packages/service` adapts.
9
+ *
10
+ * **The order is the contract.** The continuation is claimed *before* the session is touched, and the
11
+ * claim is tenant-checked, so a client reaching for a conversation it does not own never reaches the
12
+ * mutation at all. Claiming first also means a refused elevation cannot leave a spent continuation
13
+ * behind — and a spent one cannot be replayed, which is the same statement read from the other side.
14
+ */
15
+ export interface ElevationPorts {
16
+ readonly elevations: Pick<AssistantElevationStore, 'claim'>;
17
+ readonly elevateSession: (input: {
18
+ readonly sessionId: string;
19
+ readonly caller: AssistantSessionRecord['caller'];
20
+ readonly clientId: string;
21
+ readonly origin: string;
22
+ readonly customerRouting?: AssistantSessionRecord['customerRouting'];
23
+ readonly pendingResume?: AssistantPendingResume;
24
+ readonly now: Date;
25
+ }) => Promise<{
26
+ readonly ok: true;
27
+ readonly session: AssistantSessionRecord;
28
+ readonly token: string;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly reason: 'unknown_session' | 'already_elevated';
32
+ }>;
33
+ readonly now: () => Date;
34
+ }
35
+ export interface ElevationRequest {
36
+ /** Presented by the customer's backend. Possession alone is not enough; see the route. */
37
+ readonly continuation: string;
38
+ /** The tenant of the *authenticated client*, never a value the caller supplied loose. */
39
+ readonly tenant: TenantRef;
40
+ readonly caller: AssistantSessionRecord['caller'];
41
+ /** The authenticated client's id: rebinds the session's issuer basis on success (ADR 0152). */
42
+ readonly clientId: string;
43
+ /**
44
+ * Where the conversation continues: allowlist-validated by the route BEFORE the ticket is
45
+ * claimed, then re-pinned onto the session so its CORS follows the visitor's post-login page.
46
+ */
47
+ readonly origin: string;
48
+ /** Backend-verified customer routes, validated by the route; absent means unchanged. */
49
+ readonly customerRouting?: AssistantSessionRecord['customerRouting'];
50
+ /**
51
+ * Arm the one-shot resume of the intercepted tool on success. Only an explicit `true` arms —
52
+ * the route owns the default policy, and probes (the doctor's synthetic elevation) pass nothing.
53
+ */
54
+ readonly resume?: boolean;
55
+ }
56
+ export type ElevationResult = {
57
+ readonly ok: true;
58
+ readonly session: AssistantSessionRecord;
59
+ readonly token: string;
60
+ /** What the visitor was refused before signing in, for the audit trail. */
61
+ readonly tool: string;
62
+ } | {
63
+ readonly ok: false;
64
+ readonly status: 403 | 409;
65
+ readonly code: 'elevation_ticket_invalid' | 'elevation_ticket_expired' | 'elevation_tenant_mismatch' | 'elevation_session_unavailable' | 'elevation_already_signed_in';
66
+ };
67
+ export declare function completeElevation(request: ElevationRequest, ports: ElevationPorts): Promise<ElevationResult>;
68
+ //# sourceMappingURL=elevation.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The browser embed script, read from the published `@noodleseed/assistant` build.
3
+ *
4
+ * Served from the service origin rather than a CDN (ADR 0201): there is no new release lane to run, the
5
+ * script is always the one that matches the deployed service, and a CDN can front the URL later without
6
+ * changing a single byte a customer pasted.
7
+ *
8
+ * Read once and cached. The file is immutable for the life of the process — it ships inside the image —
9
+ * so re-reading it per request would buy nothing but syscalls.
10
+ */
11
+ export interface EmbedScript {
12
+ readonly bytes: Buffer;
13
+ /** Content-derived, so a redeployed service with an unchanged bundle keeps browsers' caches warm. */
14
+ readonly etag: string;
15
+ }
16
+ export type EmbedScriptResult = {
17
+ readonly ok: true;
18
+ readonly script: EmbedScript;
19
+ } | {
20
+ readonly ok: false;
21
+ readonly reason: string;
22
+ };
23
+ export declare function loadEmbedScript(): EmbedScriptResult;
24
+ //# sourceMappingURL=embed-script.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Public embed identifiers.
3
+ *
4
+ * These live in their own table rather than as a flag on the authenticated `assistant_clients` row, and
5
+ * the reason is structural rather than tidiness: a public embed id is printed in page source, so it must
6
+ * never be a credential. A record with no secret column cannot accidentally acquire one — there is
7
+ * nothing for a future code path to populate, hash, or compare. Reusing the client table would have
8
+ * inherited its tooling for free while keeping a `secret_hash` one bug away from a public identifier.
9
+ *
10
+ * The id is stable across deploys. A customer pastes the snippet once; redeploys and rollbacks swap the
11
+ * projection underneath a page that never changes. Origins, capabilities, and UI therefore come from the
12
+ * *active* deployment at mint time, never from the embed record.
13
+ */
14
+ export interface PublicEmbedRecord {
15
+ /** Printed in page source. Non-secret by construction. */
16
+ readonly embedId: string;
17
+ readonly org: string;
18
+ readonly app: string;
19
+ readonly env: string;
20
+ /**
21
+ * Which surface of the assistant this id addresses, so budgets and kill switches key per surface.
22
+ * Provisioning-time record only: `ensure` never updates a live row, so this goes stale when a
23
+ * redeploy changes the mode — readers report the active deployment's mode instead.
24
+ */
25
+ readonly surfaceMode: 'public' | 'mixed';
26
+ readonly createdAt: Date;
27
+ readonly revokedAt?: Date;
28
+ /**
29
+ * Operator overrides for this surface's daily caps. Undefined means "use the deployment defaults";
30
+ * `0` is a real value and is the kill switch, so the two must stay distinguishable everywhere.
31
+ */
32
+ readonly turnsPerDay?: number;
33
+ readonly mintsPerDay?: number;
34
+ }
35
+ /** What an operator may change about a surface. An omitted field leaves that cap as it was. */
36
+ export interface PublicEmbedBudget {
37
+ readonly turnsPerDay?: number;
38
+ readonly mintsPerDay?: number;
39
+ }
40
+ export interface PublicEmbedStore {
41
+ /**
42
+ * Return the existing id for this tenant surface, or create one. Deploy calls this, so it must be
43
+ * idempotent: a redeploy that minted a fresh id would silently break every page already carrying the
44
+ * old one.
45
+ */
46
+ ensure(input: {
47
+ readonly org: string;
48
+ readonly app: string;
49
+ readonly env: string;
50
+ readonly surfaceMode: 'public' | 'mixed';
51
+ readonly now: Date;
52
+ }): Promise<PublicEmbedRecord>;
53
+ /** Resolve an id presented by a browser. Revoked ids resolve to undefined. */
54
+ lookup(embedId: string): Promise<PublicEmbedRecord | undefined>;
55
+ /**
56
+ * Raise, lower, or switch off a surface's daily caps. Separate from `ensure` on purpose: `ensure` runs
57
+ * on every deploy and never overwrites a live row, so a redeploy cannot reset an operator's decision.
58
+ */
59
+ setBudget(embedId: string, budget: PublicEmbedBudget, now: Date): Promise<PublicEmbedRecord | undefined>;
60
+ list(tenant: {
61
+ readonly org: string;
62
+ readonly app: string;
63
+ readonly env: string;
64
+ }): Promise<readonly PublicEmbedRecord[]>;
65
+ revoke(embedId: string, now: Date): Promise<boolean>;
66
+ }
67
+ /**
68
+ * `pub_` prefixed so it is self-describing in page source, logs, and support threads — someone reading a
69
+ * customer's HTML can tell at a glance that it is not a secret that leaked.
70
+ */
71
+ export declare const PUBLIC_EMBED_ID_PREFIX = "pub_";
72
+ export declare function isPublicEmbedId(value: string): boolean;
73
+ //# sourceMappingURL=embed-store.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { type AssistantElevationClaim, type AssistantElevationRecord, type AssistantElevationStore } from './elevation-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 InMemoryAssistantElevationStore implements AssistantElevationStore {
5
+ #private;
6
+ request(input: {
7
+ readonly sessionId: string;
8
+ readonly tenant: TenantRef;
9
+ readonly tool: string;
10
+ readonly now: Date;
11
+ }): Promise<{
12
+ readonly elevation: AssistantElevationRecord;
13
+ readonly continuation: string;
14
+ }>;
15
+ claim(input: {
16
+ readonly continuation: string;
17
+ readonly tenant: TenantRef;
18
+ readonly now: Date;
19
+ }): Promise<AssistantElevationClaim>;
20
+ }
21
+ //# sourceMappingURL=in-memory-elevation-store.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { type PublicEmbedBudget, type PublicEmbedRecord, type PublicEmbedStore } from './embed-store.js';
2
+ /** 120 bits of randomness, base36: unguessable enough that an id is never a discovery oracle. */
3
+ export declare function newPublicEmbedId(): string;
4
+ export declare function tenantKey(tenant: {
5
+ readonly org: string;
6
+ readonly app: string;
7
+ readonly env: string;
8
+ }): string;
9
+ /** Process-local embeds for local development and tests. */
10
+ export declare class InMemoryPublicEmbedStore implements PublicEmbedStore {
11
+ #private;
12
+ ensure(input: {
13
+ readonly org: string;
14
+ readonly app: string;
15
+ readonly env: string;
16
+ readonly surfaceMode: 'public' | 'mixed';
17
+ readonly now: Date;
18
+ }): Promise<PublicEmbedRecord>;
19
+ setBudget(embedId: string, budget: PublicEmbedBudget, _now: Date): Promise<PublicEmbedRecord | undefined>;
20
+ lookup(embedId: string): Promise<PublicEmbedRecord | undefined>;
21
+ list(tenant: {
22
+ readonly org: string;
23
+ readonly app: string;
24
+ readonly env: string;
25
+ }): Promise<readonly PublicEmbedRecord[]>;
26
+ revoke(embedId: string, now: Date): Promise<boolean>;
27
+ }
28
+ //# sourceMappingURL=in-memory-embed-store.d.ts.map
@@ -0,0 +1,24 @@
1
+ /** Assistant gateway contracts and in-memory adapters safe for the published CLI runtime. */
2
+ export * from './artifact-projection.js';
3
+ export * from './assistant-context.js';
4
+ export * from './assistant-customer-issuer.js';
5
+ export * from './assistant-customer-routing.js';
6
+ export * from './assistant-guide.js';
7
+ export * from './assistant-interaction-state.js';
8
+ export * from './assistant-interactive.js';
9
+ export * from './assistant-presentation.js';
10
+ export * from './assistant-sensitive-values.js';
11
+ export * from './assistant-store.js';
12
+ export * from './elevation.js';
13
+ export * from './elevation-store.js';
14
+ export * from './embed-script.js';
15
+ export * from './embed-store.js';
16
+ export * from './in-memory-elevation-store.js';
17
+ export * from './in-memory-embed-store.js';
18
+ export * from './public-session.js';
19
+ export * from './public-surface.js';
20
+ export * from './public-turn.js';
21
+ export * from './session-resume.js';
22
+ export * from './surface-budget.js';
23
+ export * from './tenant-ref.js';
24
+ //# sourceMappingURL=portable.d.ts.map
@@ -0,0 +1,56 @@
1
+ import type { AdmissionEnvelope, DailyCounterStore } from '@noodle-borg/admission-limits/portable';
2
+ import { type PublicEmbedRecord, type PublicEmbedStore } from './embed-store.js';
3
+ import type { PublicSurface } from './public-surface.js';
4
+ /**
5
+ * Minting an anonymous session for a public page.
6
+ *
7
+ * Transport-free on purpose: the hosting service owns the HTTP adapter (parse body, map result), and
8
+ * everything that decides *whether* a stranger gets a session lives here, behind narrow ports. That is
9
+ * what keeps the decision testable without a server and the service-side residue to a few lines.
10
+ *
11
+ * The refusal order is part of the contract, not an implementation detail. ADR 0201 §8 requires
12
+ * admission to complete before any metered work, so the store-durability and budget checks come before
13
+ * anything that would read deployment state or reach a model.
14
+ */
15
+ export interface PublicSessionPorts {
16
+ readonly embeds: Pick<PublicEmbedStore, 'lookup'>;
17
+ readonly counters: DailyCounterStore;
18
+ /** The live surface for this tenant, read from the *active* deployment at mint time. */
19
+ resolveActiveSurface(embed: PublicEmbedRecord): Promise<PublicSurface | undefined>;
20
+ /** Persist the anonymous session and return its bearer token and expiry. */
21
+ createSession(input: {
22
+ readonly embed: PublicEmbedRecord;
23
+ readonly origin: string;
24
+ readonly subject: string;
25
+ readonly envelope: AdmissionEnvelope;
26
+ }): Promise<{
27
+ readonly token: string;
28
+ readonly expiresAt: string;
29
+ }>;
30
+ /** Opaque, deployment-scoped, unlinkable to any person. */
31
+ newAnonymousSubject(): string;
32
+ now(): Date;
33
+ }
34
+ export interface PublicSessionRequest {
35
+ readonly embedId: unknown;
36
+ readonly origin: unknown;
37
+ /**
38
+ * The visitor's address bucket (admission tier 3), already hashed by the transport — a raw address
39
+ * must never reach this decision, a counter key, or an audit payload. Absent when ingress could not
40
+ * be parsed, in which case the surface tier alone applies: fairness degrades, solvency does not.
41
+ */
42
+ readonly addressBucket?: string | undefined;
43
+ }
44
+ export type PublicSessionResult = {
45
+ readonly ok: true;
46
+ readonly token: string;
47
+ readonly expiresAt: string;
48
+ readonly embed: PublicEmbedRecord;
49
+ } | {
50
+ readonly ok: false;
51
+ readonly status: number;
52
+ readonly code: string;
53
+ readonly message: string;
54
+ };
55
+ export declare function mintPublicSession(request: PublicSessionRequest, envelope: AdmissionEnvelope, ports: PublicSessionPorts): Promise<PublicSessionResult>;
56
+ //# sourceMappingURL=public-session.d.ts.map
@@ -0,0 +1,21 @@
1
+ import type { SurfaceCapabilityRef } from './artifact-projection.js';
2
+ /**
3
+ * The one reader of "does this deployment face the public, and on what terms".
4
+ *
5
+ * Three callers ask that question and must never disagree: deploy decides whether to provision an embed
6
+ * id, minting checks the browser's origin against the surface, and the session's projection narrows the
7
+ * artifact to the surface's capabilities. When each answered locally, a surface shape one of them
8
+ * accepted and another rejected would silently mint sessions that could reach nothing, or provision an id
9
+ * for a surface that refuses to serve it.
10
+ *
11
+ * The argument is `unknown` because the assistant block arrives as compiled artifact data rather than a
12
+ * typed authoring value; the shape is validated here, once, rather than asserted at three call sites.
13
+ */
14
+ export interface PublicSurface {
15
+ readonly mode: 'public' | 'mixed';
16
+ readonly origins: readonly string[];
17
+ readonly capabilities: readonly SurfaceCapabilityRef[];
18
+ readonly instructions?: string;
19
+ }
20
+ export declare function publicSurfaceOf(assistant: unknown): PublicSurface | undefined;
21
+ //# sourceMappingURL=public-surface.d.ts.map
@@ -3,7 +3,7 @@ export function publicSurfaceOf(assistant) {
3
3
  if (!Array.isArray(surfaces))
4
4
  return undefined;
5
5
  for (const entry of surfaces) {
6
- const { mode, origins, capabilities } = entry;
6
+ const { mode, origins, capabilities, instructions } = entry;
7
7
  // A mixed surface admits strangers exactly as a public one does — sign-in only widens what a visitor
8
8
  // reaches after elevation, so both provision an embed id and both project.
9
9
  if (mode !== 'public' && mode !== 'mixed')
@@ -17,6 +17,7 @@ export function publicSurfaceOf(assistant) {
17
17
  capabilities: Array.isArray(capabilities)
18
18
  ? capabilities
19
19
  : [],
20
+ ...(typeof instructions === 'string' ? { instructions } : {}),
20
21
  };
21
22
  }
22
23
  return undefined;