@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
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { MAX_COMPILED_WIDGET_HTML_BYTES, MAX_RAW_WIDGET_HTML_BYTES } from '../widget-limits.js';
4
4
  import { agentGuideSchema } from './agent-guide-schema.js';
5
5
  import { serverAuthSchema, serverAuthV2Schema } from './auth-schema.js';
6
+ import { assistantOriginOrVariableSchema, httpsUrlSchema, managedVariableExpressionSchema, serverBrandingSchema, } from './branding-schema.js';
6
7
  import { NAME_PATTERN } from './naming.js';
7
8
  /**
8
9
  * Zod schema for Core v1 and v2 manifests (ADRs 0150 and 0157). V1 remains accepted unchanged;
@@ -133,82 +134,6 @@ const promptSchema = z.object({
133
134
  arguments: z.array(promptArgumentSchema).optional(),
134
135
  fulfilment: fulfilmentSchema,
135
136
  });
136
- const hexColorSchema = z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'must be a 6-digit hex color');
137
- const httpsUrlSchema = z.url().regex(/^https:\/\//, 'must use https');
138
- const managedVariableExpressionPattern = /^\$\{env\.[A-Za-z0-9_]+\}$/;
139
- const managedVariableExpressionSchema = z
140
- .string()
141
- .regex(managedVariableExpressionPattern, 'must be an exact managed variable expression');
142
- // HTTP is allowed only for loopback development origins (mirrors the host-pattern loopback
143
- // exception used for connector/CSP origins); production embedding origins stay https-only.
144
- const assistantOriginOrVariablePattern = /^(?:\$\{env\.[A-Za-z0-9_]+\}|https:\/\/[^/?#@\\]+|http:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?)$/;
145
- function isCanonicalAssistantOrigin(value) {
146
- try {
147
- return new URL(value).origin === value;
148
- }
149
- catch {
150
- return false;
151
- }
152
- }
153
- const assistantOriginOrVariableSchema = z
154
- .string()
155
- .regex(assistantOriginOrVariablePattern, 'must be a canonical bare origin using https (http is allowed only for localhost/127.0.0.1/[::1] loopback development origins)')
156
- .refine((value) => managedVariableExpressionPattern.test(value) || isCanonicalAssistantOrigin(value), 'must be a canonical bare origin');
157
- const packagedAssetSchema = z
158
- .object({
159
- kind: z.literal('asset'),
160
- sourcePath: z.string().min(1),
161
- logicalId: z.string().min(1),
162
- })
163
- .strict();
164
- const brandAssetSchema = z
165
- .object({
166
- uri: z.union([httpsUrlSchema, packagedAssetSchema]),
167
- darkUri: z.union([httpsUrlSchema, packagedAssetSchema]).optional(),
168
- alt: z.string().trim().min(1),
169
- })
170
- .strict();
171
- const brandThemeTokensSchema = z
172
- .object({
173
- surface: hexColorSchema.optional(),
174
- surfaceRaised: hexColorSchema.optional(),
175
- surfaceMuted: hexColorSchema.optional(),
176
- text: hexColorSchema.optional(),
177
- textMuted: hexColorSchema.optional(),
178
- accent: hexColorSchema.optional(),
179
- accentText: hexColorSchema.optional(),
180
- link: hexColorSchema.optional(),
181
- border: hexColorSchema.optional(),
182
- borderStrong: hexColorSchema.optional(),
183
- focus: hexColorSchema.optional(),
184
- success: hexColorSchema.optional(),
185
- warning: hexColorSchema.optional(),
186
- danger: hexColorSchema.optional(),
187
- code: hexColorSchema.optional(),
188
- })
189
- .strict();
190
- const serverBrandingSchema = z
191
- .object({
192
- name: z.string().trim().min(1).optional(),
193
- accent: hexColorSchema.optional(),
194
- surface: hexColorSchema.optional(),
195
- surfaceDark: hexColorSchema.optional(),
196
- logo: brandAssetSchema.optional(),
197
- mark: brandAssetSchema.optional(),
198
- avatar: brandAssetSchema.optional(),
199
- theme: z
200
- .object({
201
- light: brandThemeTokensSchema.optional(),
202
- dark: brandThemeTokensSchema.optional(),
203
- })
204
- .strict()
205
- .optional(),
206
- radius: z.enum(['none', 'sm', 'md', 'lg']).optional(),
207
- density: z.enum(['compact', 'comfortable']).optional(),
208
- typography: z.enum(['system', 'serif', 'mono']).optional(),
209
- colorScheme: z.enum(['auto', 'light', 'dark']).optional(),
210
- })
211
- .strict();
212
137
  const assistantPresentationToneSchema = z.enum(['neutral', 'success', 'warning', 'danger']);
213
138
  const optionalStrictObject = (shape) => z.object(shape).strict().optional();
214
139
  const assistantPresentationSchema = z
@@ -357,6 +282,18 @@ const embeddedAssistantSchema = z
357
282
  // `assistantUiSchema.shape` — this is not renderer presentation, and a surface that
358
283
  // acquired it by a spread would be an accident rather than a decision.
359
284
  webmcp: optionalStrictObject({ enabled: z.boolean().optional() }),
285
+ // Anonymous cross-page display continuity (ADR 0223, clauses 11-16), declared per public
286
+ // or mixed surface. These bounds mirror the structural ceilings the gateway clamps to at
287
+ // runtime; the ADR is the single source, and this package cannot import the gateway
288
+ // because the dependency runs the other way. Refusing rather than clamping is deliberate:
289
+ // the gateway narrows an operator's value silently by design, but a developer who asks
290
+ // for more than the ceiling has made a mistake, and silence would hide it until someone
291
+ // measured the live behaviour.
292
+ continuity: optionalStrictObject({
293
+ enabled: z.boolean().optional(),
294
+ windowSeconds: z.number().int().min(0).max(600).optional(),
295
+ maxRestores: z.number().int().min(0).max(10).optional(),
296
+ }),
360
297
  })
361
298
  .strict())
362
299
  .min(1)
@@ -374,6 +311,17 @@ const embeddedAssistantSchema = z
374
311
  .strict()
375
312
  .superRefine((assistant, ctx) => {
376
313
  assistant.surfaces?.forEach((surface, index) => {
314
+ // The authenticated direction already reattaches through ADR 0223 clause 7, authorized by a
315
+ // backend-verified subject rather than by possession of a handle. A `continuity` block here
316
+ // would be a developer believing they configured something nothing reads, so refuse it rather
317
+ // than drop it silently.
318
+ if (surface.mode === 'authenticated' && surface.continuity !== undefined) {
319
+ ctx.addIssue({
320
+ code: 'custom',
321
+ path: ['surfaces', index, 'continuity'],
322
+ message: 'continuity belongs on a public or mixed website surface',
323
+ });
324
+ }
377
325
  if (surface.mode === 'authenticated')
378
326
  return;
379
327
  // A public surface fails closed: an omitted allowlist would otherwise read as "expose
@@ -28,7 +28,6 @@
28
28
  "@noodle-borg/uri-template": "0.0.0",
29
29
  "@noodle-borg/knowledge": "0.0.0",
30
30
  "@noodle-borg/capabilities": "0.0.0",
31
- "tailwindcss": "^4.3.1",
32
31
  "tldts": "^7.4.9",
33
32
  "yaml": "^2.9.0",
34
33
  "zod": "^4.4.3"
@@ -9,8 +9,12 @@ function largestWidgetContributors(manifest) {
9
9
  try {
10
10
  const parsed = JSON.parse(manifest);
11
11
  const contributors = (parsed.widgets ?? [])
12
- .filter((widget) => typeof widget.name === 'string' && typeof widget.html === 'string')
13
- .map((widget) => ({ name: widget.name, bytes: Buffer.byteLength(widget.html) }))
12
+ .flatMap((widget) => {
13
+ const html = widget.html ?? widget.view?.compiledHtml;
14
+ return typeof widget.name === 'string' && typeof html === 'string'
15
+ ? [{ name: widget.name, bytes: Buffer.byteLength(html) }]
16
+ : [];
17
+ })
14
18
  .sort((left, right) => right.bytes - left.bytes)
15
19
  .slice(0, 3);
16
20
  return contributors.length === 0
@@ -15,7 +15,7 @@ export type ActiveDeployProvenance = {
15
15
  readonly ownerSubject: string | undefined;
16
16
  } | undefined;
17
17
  /** Resolve the durable owner binding while keeping legacy creator-only records readable. */
18
- export declare function deploymentOwnerSubject(record: DeployRecord): string | undefined;
18
+ export declare function deploymentOwnerSubject(record: Pick<DeployRecord, 'ownerSubject' | 'createdBySubject'>): string | undefined;
19
19
  export declare function missingCapabilityErrors(required: readonly CapabilityName[], available: readonly CapabilityName[]): readonly DeployError[];
20
20
  export declare function missingSecretErrors(bindings: readonly SecretBinding[], secrets: Readonly<Record<string, string>>): DeployError[];
21
21
  export declare function missingVariableErrors(bindings: readonly string[], variables: Readonly<Record<string, string>>): DeployError[];
@@ -1,14 +1,24 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { cspFaultsInManifest } from '@noodle-borg/compiler';
2
3
  import { readDeployBody, sendJson } from '@noodle-borg/transport-http';
3
- import { deployPreflightRequestSchema, formatWireError } from '@noodle-borg/wire-contracts';
4
+ import { DEPLOY_PREFLIGHT_CHECK_TIMEOUT_MS, deployPreflightRequestSchema, formatWireError, } from '@noodle-borg/wire-contracts';
4
5
  import { manifestUsesUserRoot } from '../registry-access.js';
5
6
  import { isIdentityAccessMode } from './access-mode.js';
6
7
  import { authorizeDeploymentOwnerSelection, authorizeTenantControl, developerGrantRouteAccess, } from './control-plane.js';
8
+ // Per-process resource admission, not a tenant quota or distributed state authority.
9
+ const activePreflights = new WeakSet();
7
10
  /**
8
11
  * Read-only first-deploy readiness. It intentionally does not verify uploaded asset bytes:
9
12
  * the CLI calls this before it requests upload targets or sends any bytes.
10
13
  */
11
14
  export async function handleDeployPreflight(req, res, registry, options, maxBody, tenant, gate, controlPlane, audit) {
15
+ const startedAt = Date.now();
16
+ const suppliedId = req.headers['x-request-id'];
17
+ const requestId = typeof suppliedId === 'string' &&
18
+ /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(suppliedId)
19
+ ? suppliedId
20
+ : randomUUID();
21
+ res.setHeader('x-request-id', requestId);
12
22
  const identity = await authorizeTenantControl(req, res, gate, controlPlane, tenant.org, developerGrantRouteAccess(options.developerGrantStore, 'deployments:write'));
13
23
  if (identity === false)
14
24
  return;
@@ -25,111 +35,198 @@ export async function handleDeployPreflight(req, res, registry, options, maxBody
25
35
  error: 'app is archived; restore it before deploying',
26
36
  });
27
37
  }
28
- const body = await readDeployBody(req, maxBody);
29
- if (!body.ok)
30
- return sendJson(res, body.status, { error: body.error });
31
- let parsed;
32
- try {
33
- const raw = JSON.parse(body.text);
34
- const result = deployPreflightRequestSchema.safeParse(raw);
35
- if (!result.success)
36
- throw new Error(formatWireError(result.error));
37
- parsed = result.data;
38
- }
39
- catch (error) {
40
- return sendJson(res, 400, {
41
- code: 'invalid_deploy_request',
42
- error: `invalid deploy preflight request: ${error.message}`,
38
+ if (activePreflights.has(registry)) {
39
+ await audit.emit({
40
+ eventType: 'deploy.preflight.rejected',
41
+ ...tenant,
42
+ actorSubject: identity.subject,
43
+ decision: 'deny',
44
+ status: 503,
45
+ reasonCode: 'deploy_preflight_busy',
46
+ details: { requestId, phase: 'admission', elapsedMs: Date.now() - startedAt },
43
47
  });
44
- }
45
- const accessMode = parsed.accessMode ?? 'owner-only';
46
- if (isIdentityAccessMode(accessMode) && identity === undefined) {
47
- return sendJson(res, 401, {
48
- code: 'deployer_identity_required',
49
- error: `${accessMode} deployments require an authenticated deployer`,
48
+ res.setHeader('retry-after', '5');
49
+ return sendJson(res, 503, {
50
+ code: 'deploy_preflight_busy',
51
+ phase: 'admission',
52
+ error: 'This instance is already checking a deployment. Retry once after the indicated delay.',
50
53
  });
51
54
  }
52
- const ownerSelection = await authorizeDeploymentOwnerSelection(res, controlPlane, tenant.org, accessMode, identity, parsed.ownerSubject);
53
- if (!ownerSelection.ok)
54
- return;
55
- if (accessMode === 'public' && manifestUsesUserRoot(parsed.manifest)) {
56
- return sendJson(res, 400, {
57
- code: 'public_user_context_conflict',
58
- error: 'public access mode cannot reference ${user}; use mixed for optional identity',
55
+ activePreflights.add(registry);
56
+ let clientDisconnected = false;
57
+ let deadlineExceeded = false;
58
+ let phase = 'body';
59
+ let timer;
60
+ const onClose = () => {
61
+ if (!res.writableFinished)
62
+ clientDisconnected = true;
63
+ };
64
+ res.once('close', onClose);
65
+ try {
66
+ await audit.emit({
67
+ eventType: 'deploy.preflight.started',
68
+ ...tenant,
69
+ actorSubject: identity.subject,
70
+ decision: 'allow',
71
+ details: { requestId, phase, elapsedMs: Date.now() - startedAt },
59
72
  });
60
- }
61
- const [cspFault] = cspFaultsInManifest(parsed.manifest);
62
- if (cspFault !== undefined) {
63
- return sendJson(res, 400, {
64
- code: 'invalid_widget_csp',
65
- error: `widget "${cspFault.widget}" CSP ${cspFault.list} origin "${cspFault.value}" is not an ` +
66
- `absolute https:// origin and would be dropped by the host renderer` +
67
- (cspFault.suggestion !== undefined ? `; use "${cspFault.suggestion}"` : ''),
73
+ const body = await readDeployBody(req, maxBody);
74
+ if (req.aborted || res.destroyed)
75
+ return;
76
+ if (!body.ok)
77
+ return sendJson(res, body.status, { error: body.error });
78
+ phase = 'validation';
79
+ let parsed;
80
+ try {
81
+ const raw = JSON.parse(body.text);
82
+ const result = deployPreflightRequestSchema.safeParse(raw);
83
+ if (!result.success)
84
+ throw new Error(formatWireError(result.error));
85
+ parsed = result.data;
86
+ }
87
+ catch (error) {
88
+ return sendJson(res, 400, {
89
+ code: 'invalid_deploy_request',
90
+ error: `invalid deploy preflight request: ${error.message}`,
91
+ });
92
+ }
93
+ const accessMode = parsed.accessMode ?? 'owner-only';
94
+ if (isIdentityAccessMode(accessMode) && identity === undefined) {
95
+ return sendJson(res, 401, {
96
+ code: 'deployer_identity_required',
97
+ error: `${accessMode} deployments require an authenticated deployer`,
98
+ });
99
+ }
100
+ const ownerSelection = await authorizeDeploymentOwnerSelection(res, controlPlane, tenant.org, accessMode, identity, parsed.ownerSubject);
101
+ if (!ownerSelection.ok)
102
+ return;
103
+ if (accessMode === 'public' && manifestUsesUserRoot(parsed.manifest)) {
104
+ return sendJson(res, 400, {
105
+ code: 'public_user_context_conflict',
106
+ error: 'public access mode cannot reference ${user}; use mixed for optional identity',
107
+ });
108
+ }
109
+ const [cspFault] = cspFaultsInManifest(parsed.manifest);
110
+ if (cspFault !== undefined) {
111
+ return sendJson(res, 400, {
112
+ code: 'invalid_widget_csp',
113
+ error: `widget "${cspFault.widget}" CSP ${cspFault.list} origin "${cspFault.value}" is not an ` +
114
+ `absolute https:// origin and would be dropped by the host renderer` +
115
+ (cspFault.suggestion !== undefined ? `; use "${cspFault.suggestion}"` : ''),
116
+ });
117
+ }
118
+ phase = 'checks';
119
+ timer = setTimeout(() => {
120
+ deadlineExceeded = true;
121
+ if (!res.headersSent && !res.destroyed)
122
+ sendJson(res, 504, {
123
+ code: 'deploy_preflight_timeout',
124
+ phase,
125
+ error: `Preflight exceeded its ${DEPLOY_PREFLIGHT_CHECK_TIMEOUT_MS}ms checking budget. Publication did not start.`,
126
+ });
127
+ }, DEPLOY_PREFLIGHT_CHECK_TIMEOUT_MS);
128
+ const pending = [
129
+ registry.getApp(tenant.org, tenant.app),
130
+ registry.getEnvironment(tenant.org, tenant.app, tenant.env),
131
+ registry.preflightDeploy(tenant, parsed.manifest, {
132
+ ...(parsed.connectors !== undefined ? { connectors: parsed.connectors } : {}),
133
+ actor: identity,
134
+ accessMode,
135
+ ownerSubject: ownerSelection.ownerSubject,
136
+ ...(parsed.orgMembershipSources !== undefined
137
+ ? { orgMembershipSources: parsed.orgMembershipSources }
138
+ : {}),
139
+ ...(parsed.hostedAssets !== undefined ? { hostedAssets: parsed.hostedAssets } : {}),
140
+ ...(parsed.serverVersion !== undefined ? { serverVersion: parsed.serverVersion } : {}),
141
+ deploymentSource: parsed.deploymentSource ?? 'cli',
142
+ }),
143
+ ];
144
+ const checked = await Promise.all(pending)
145
+ .catch(async (error) => {
146
+ // Keep the resource slot until every already-started check settles, including after a timeout.
147
+ await Promise.allSettled(pending);
148
+ throw error;
149
+ })
150
+ .finally(() => clearTimeout(timer));
151
+ const [application, environment, preflight] = checked;
152
+ const errors = preflight.ok ? [] : preflight.errors;
153
+ const missingSecrets = configNames(errors, 'missing_secret', 'secrets.');
154
+ const missingVariables = configNames(errors, 'missing_variable', 'variables.');
155
+ const appState = application === undefined ? 'will-create' : 'existing';
156
+ const environmentState = environment === undefined ? 'will-create' : 'existing';
157
+ const ready = errors.length === 0;
158
+ const codes = [...new Set(errors.map((error) => error.code))].sort().join(',');
159
+ await audit.emit({
160
+ eventType: 'deploy.preflight.checked',
161
+ org: tenant.org,
162
+ app: tenant.app,
163
+ env: tenant.env,
164
+ decision: 'allow',
165
+ status: 200,
166
+ actorSubject: identity.subject,
167
+ ...(identity.email !== undefined ? { actorEmail: identity.email } : {}),
168
+ details: {
169
+ requestId,
170
+ phase,
171
+ elapsedMs: Date.now() - startedAt,
172
+ clientDisconnected,
173
+ deadlineExceeded,
174
+ ready,
175
+ appState,
176
+ environmentState,
177
+ ...(ownerSelection.ownerSubject !== undefined
178
+ ? { ownerSubject: ownerSelection.ownerSubject }
179
+ : {}),
180
+ errorCount: errors.length,
181
+ codes,
182
+ missingSecretCount: missingSecrets.length,
183
+ missingVariableCount: missingVariables.length,
184
+ },
68
185
  });
69
- }
70
- const [application, environment, preflight] = await Promise.all([
71
- registry.getApp(tenant.org, tenant.app),
72
- registry.getEnvironment(tenant.org, tenant.app, tenant.env),
73
- registry.preflightDeploy(tenant, parsed.manifest, {
74
- ...(parsed.connectors !== undefined ? { connectors: parsed.connectors } : {}),
75
- actor: identity,
76
- accessMode,
77
- ownerSubject: ownerSelection.ownerSubject,
78
- ...(parsed.orgMembershipSources !== undefined
79
- ? { orgMembershipSources: parsed.orgMembershipSources }
80
- : {}),
81
- ...(parsed.hostedAssets !== undefined ? { hostedAssets: parsed.hostedAssets } : {}),
82
- ...(parsed.serverVersion !== undefined ? { serverVersion: parsed.serverVersion } : {}),
83
- deploymentSource: parsed.deploymentSource ?? 'cli',
84
- }),
85
- ]);
86
- const errors = preflight.ok ? [] : preflight.errors;
87
- const missingSecrets = configNames(errors, 'missing_secret', 'secrets.');
88
- const missingVariables = configNames(errors, 'missing_variable', 'variables.');
89
- const appState = application === undefined ? 'will-create' : 'existing';
90
- const environmentState = environment === undefined ? 'will-create' : 'existing';
91
- const ready = errors.length === 0;
92
- const codes = [...new Set(errors.map((error) => error.code))].sort().join(',');
93
- await audit.emit({
94
- eventType: 'deploy.preflight.checked',
95
- org: tenant.org,
96
- app: tenant.app,
97
- env: tenant.env,
98
- decision: 'allow',
99
- status: 200,
100
- actorSubject: identity.subject,
101
- ...(identity.email !== undefined ? { actorEmail: identity.email } : {}),
102
- details: {
186
+ if (deadlineExceeded || clientDisconnected || res.destroyed)
187
+ return;
188
+ return sendJson(res, 200, {
189
+ ok: true,
103
190
  ready,
104
- appState,
105
- environmentState,
106
191
  ...(ownerSelection.ownerSubject !== undefined
107
192
  ? { ownerSubject: ownerSelection.ownerSubject }
108
193
  : {}),
109
- errorCount: errors.length,
110
- codes,
111
- missingSecretCount: missingSecrets.length,
112
- missingVariableCount: missingVariables.length,
113
- },
114
- });
115
- return sendJson(res, 200, {
116
- ok: true,
117
- ready,
118
- ...(ownerSelection.ownerSubject !== undefined
119
- ? { ownerSubject: ownerSelection.ownerSubject }
120
- : {}),
121
- target: {
194
+ target: {
195
+ ...tenant,
196
+ appState,
197
+ environmentState,
198
+ },
199
+ config: {
200
+ ready: missingSecrets.length === 0 && missingVariables.length === 0,
201
+ missingSecrets,
202
+ missingVariables,
203
+ },
204
+ errors,
205
+ });
206
+ }
207
+ catch (error) {
208
+ await audit.emit({
209
+ eventType: 'deploy.preflight.failed',
122
210
  ...tenant,
123
- appState,
124
- environmentState,
125
- },
126
- config: {
127
- ready: missingSecrets.length === 0 && missingVariables.length === 0,
128
- missingSecrets,
129
- missingVariables,
130
- },
131
- errors,
132
- });
211
+ actorSubject: identity.subject,
212
+ decision: 'deny',
213
+ status: res.headersSent ? res.statusCode : 500,
214
+ reasonCode: 'preflight_failed',
215
+ details: {
216
+ requestId,
217
+ phase,
218
+ elapsedMs: Date.now() - startedAt,
219
+ clientDisconnected,
220
+ deadlineExceeded,
221
+ },
222
+ });
223
+ throw error;
224
+ }
225
+ finally {
226
+ clearTimeout(timer);
227
+ res.removeListener('close', onClose);
228
+ activePreflights.delete(registry);
229
+ }
133
230
  }
134
231
  function configNames(errors, code, prefix) {
135
232
  return [
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@modelcontextprotocol/sdk": "^1.29.0",
41
41
  "@noodle-borg/admission-limits": "0.0.0",
42
- "@noodle-borg/agent-kit": "0.93.0",
42
+ "@noodle-borg/agent-kit": "0.94.0",
43
43
  "@noodle-borg/app-package": "0.0.0",
44
44
  "@noodle-borg/assistant-gateway": "0.0.0",
45
45
  "@noodle-borg/auth": "0.0.0",
@@ -137,6 +137,9 @@ export declare const deployErrorResponseSchema: z.ZodObject<{
137
137
  errors: z.ZodOptional<z.ZodUnknown>;
138
138
  }, z.core.$strip>;
139
139
  export type DeployErrorResponse = z.infer<typeof deployErrorResponseSchema>;
140
+ /** Checking budget after upload/authorization; the client allows additional upload/response time. */
141
+ export declare const DEPLOY_PREFLIGHT_CHECK_TIMEOUT_MS = 60000;
142
+ export declare const DEPLOY_PREFLIGHT_CLIENT_TIMEOUT_MS = 75000;
140
143
  /**
141
144
  * `POST /v1/orgs/{org}/apps/{app}/envs/{env}/deploy/preflight` accepts the same
142
145
  * declarative input as deploy, but performs no deployment or asset upload.
@@ -109,6 +109,9 @@ export const deployErrorResponseSchema = z.object({
109
109
  errors: z.unknown().optional(),
110
110
  });
111
111
  // ─── Deploy preflight ─────────────────────────────────────────────────────────────
112
+ /** Checking budget after upload/authorization; the client allows additional upload/response time. */
113
+ export const DEPLOY_PREFLIGHT_CHECK_TIMEOUT_MS = 60_000;
114
+ export const DEPLOY_PREFLIGHT_CLIENT_TIMEOUT_MS = 75_000;
112
115
  /**
113
116
  * `POST /v1/orgs/{org}/apps/{app}/envs/{env}/deploy/preflight` accepts the same
114
117
  * declarative input as deploy, but performs no deployment or asset upload.
@@ -6,7 +6,8 @@ The package exports the canonical `<noodle-assistant>` managed Web Component, th
6
6
  `<noodle-app-view>` MCP App host from `@noodleseed/assistant/app-view`, a managed React wrapper and
7
7
  `NoodleAppView` adapter from `@noodleseed/assistant/react`, a renderer-free React hook from
8
8
  `@noodleseed/assistant/react/client`, a DOM-free client from `@noodleseed/assistant/client`, and the
9
- backend-only `createAssistantSession` helper from `@noodleseed/assistant/server`. The component inherits the
9
+ backend-only `createAssistantSessionHandler` and lower-level `createAssistantSession` exchange from
10
+ `@noodleseed/assistant/server`. The component inherits the
10
11
  deployed MCP server's brand kit while slots, methods, events, and semantic CSS variables let a SaaS developer
11
12
  integrate it without depending on internal DOM selectors. Light, dark, automatic host-page matching, and
12
13
  host-page inversion work without replacing conversation or App DOM.
@@ -193,32 +194,29 @@ noodle assistant clients create --name web --org acme --app support --env prod
193
194
 
194
195
  The command saves `{ clientId, clientSecret }` to a mode-`0600` file and prints its path, never the secret.
195
196
  Move those values to the customer backend's secret manager without printing or committing them. A framework
196
- route can exchange the current signed-in user like this:
197
+ route can use the maintained same-origin handler. Generate the route, authentication adapter, mount and
198
+ contract tests with `noodle assistant embed --framework nextjs --surface authenticated` in the existing
199
+ application. Implement only `authenticateAssistantRequest` using its existing session and membership checks:
197
200
 
198
201
  ```ts
199
- import { createAssistantSession } from "@noodleseed/assistant/server";
200
-
201
- export async function POST(request: Request) {
202
- const user = await requireCurrentUser(request);
203
- const { context } = await request.json();
204
- const session = await createAssistantSession({
205
- serviceUrl: process.env.NOODLE_SERVICE_URL!,
206
- clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,
207
- clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,
208
- origin: process.env.PUBLIC_APP_ORIGIN!,
209
- user: {
210
- id: user.id,
211
- email: user.email,
212
- roles: user.roles,
213
- scopes: user.scopes,
214
- },
215
- context,
216
- preferences: { locale: user.locale, timeZone: user.timeZone },
217
- });
218
- return Response.json(session);
219
- }
202
+ import { createAssistantSessionHandler } from "@noodleseed/assistant/server";
203
+ import { authenticateAssistantRequest } from "../../../../lib/noodle-assistant-auth";
204
+
205
+ export const POST = createAssistantSessionHandler({
206
+ serviceUrl: process.env.NOODLE_SERVICE_URL,
207
+ clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID,
208
+ clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET,
209
+ origin: process.env.PUBLIC_APP_ORIGIN,
210
+ authenticate: authenticateAssistantRequest,
211
+ });
220
212
  ```
221
213
 
214
+ The handler owns exact-Origin and JSON checks, signed-out JSON 401, bounded context/body parsing, no-store
215
+ responses, a 15-second deadline, and sanitized exchange failures without retries or redirects. Your adapter
216
+ returns `AssistantSessionIdentity | null`; its `user`, claims, preferences and routing must be backend-verified.
217
+ Keep stricter application CSRF middleware. Run the generated contract suite and your real identity/tenant
218
+ acceptance tests; synthetic contract tests alone do not verify your login implementation.
219
+
222
220
  For a connector that uses `customerEndpoint("customer_api", ...)`, the authenticated backend may also pass
223
221
  `routing: { endpoints: { customer_api: account.clusterApiBaseUrl } }`. Resolve the URL from server-owned
224
222
  user/account membership after authentication; never accept it from the browser, page context, session
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/assistant",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "Embed the Noodle Seed customer-branded assistant in your web app with managed or framework-owned UI, a framework-neutral MCP App host, a DOM-free client, and a backend session helper. Authoring and deploying the server is @noodleseed/one.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/one",
3
- "version": "0.154.0",
3
+ "version": "0.156.0",
4
4
  "private": false,
5
5
  "description": "Noodle CLI by Noodle Seed — author, run, and deploy declarative MCP servers. Embedding the assistant in your own web app is @noodleseed/assistant.",
6
6
  "license": "Apache-2.0",
@@ -205,7 +205,6 @@
205
205
  "shebang-command",
206
206
  "shebang-regex",
207
207
  "statuses",
208
- "tailwindcss",
209
208
  "tldts",
210
209
  "tldts-core",
211
210
  "toidentifier",
@@ -236,7 +235,7 @@
236
235
  "@modelcontextprotocol/client": "2.0.0",
237
236
  "@modelcontextprotocol/server": "2.0.0",
238
237
  "@noodle-borg/admission-limits": "0.0.0",
239
- "@noodle-borg/agent-kit": "0.93.0",
238
+ "@noodle-borg/agent-kit": "0.94.0",
240
239
  "@noodle-borg/app-audit": "0.0.0",
241
240
  "@noodle-borg/app-package": "0.0.0",
242
241
  "@noodle-borg/assistant-gateway": "0.0.0",
@@ -285,7 +284,9 @@
285
284
  "zod-to-json-schema": "3.25.2"
286
285
  },
287
286
  "devDependencies": {
288
- "@types/node": "24.13.1"
287
+ "@noodleseed/assistant": "@noodleseed/assistant@file:///home/runner/work/noodle-borg/noodle-borg/packages/assistant",
288
+ "@types/node": "24.13.1",
289
+ "next": "16.2.9"
289
290
  },
290
291
  "optionalDependencies": {}
291
292
  }
@@ -1,4 +0,0 @@
1
- import type { ProjectAgentTarget } from './project.js';
2
- /** Default SaaS starter: the portable Apps surface plus customer identity and verified context. */
3
- export declare function saasFiles(name: string, agentTargets: readonly ProjectAgentTarget[]): Record<string, string>;
4
- //# sourceMappingURL=saas-scaffold-template.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"saas-scaffold-template.d.ts","sourceRoot":"","sources":["../src/saas-scaffold-template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGvD,mGAAmG;AACnG,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,kBAAkB,EAAE,GAC1C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6DxB"}