@noodleseed/one 0.155.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.
- package/README.md +5 -1
- package/dist/assistant-embed-django-tests.d.ts +3 -0
- package/dist/assistant-embed-django-tests.d.ts.map +1 -0
- package/dist/assistant-embed-django-tests.js +140 -0
- package/dist/assistant-embed-django-tests.js.map +1 -0
- package/dist/assistant-embed-django.d.ts +4 -0
- package/dist/assistant-embed-django.d.ts.map +1 -0
- package/dist/assistant-embed-django.js +259 -0
- package/dist/assistant-embed-django.js.map +1 -0
- package/dist/assistant-embed-installation.d.ts +29 -0
- package/dist/assistant-embed-installation.d.ts.map +1 -0
- package/dist/assistant-embed-installation.js +124 -0
- package/dist/assistant-embed-installation.js.map +1 -0
- package/dist/assistant-embed-profile.d.ts +8 -0
- package/dist/assistant-embed-profile.d.ts.map +1 -0
- package/dist/assistant-embed-profile.js +150 -0
- package/dist/assistant-embed-profile.js.map +1 -0
- package/dist/assistant-embed-scaffold-template.d.ts +3 -2
- package/dist/assistant-embed-scaffold-template.d.ts.map +1 -1
- package/dist/assistant-embed-scaffold-template.js +39 -84
- package/dist/assistant-embed-scaffold-template.js.map +1 -1
- package/dist/assistant-embed-tests.d.ts +5 -0
- package/dist/assistant-embed-tests.d.ts.map +1 -0
- package/dist/assistant-embed-tests.js +106 -0
- package/dist/assistant-embed-tests.js.map +1 -0
- package/dist/assistant-embed-vue.d.ts +3 -0
- package/dist/assistant-embed-vue.d.ts.map +1 -0
- package/dist/assistant-embed-vue.js +77 -0
- package/dist/assistant-embed-vue.js.map +1 -0
- package/dist/commands/assistant-embed-ops.d.ts.map +1 -1
- package/dist/commands/assistant-embed-ops.js +112 -63
- package/dist/commands/assistant-embed-ops.js.map +1 -1
- package/dist/commands/author-loop.d.ts.map +1 -1
- package/dist/commands/author-loop.js +24 -28
- package/dist/commands/author-loop.js.map +1 -1
- package/dist/commands/deploy-first-flow.d.ts.map +1 -1
- package/dist/commands/deploy-first-flow.js +26 -15
- package/dist/commands/deploy-first-flow.js.map +1 -1
- package/dist/commands/deploy-preflight.d.ts +5 -0
- package/dist/commands/deploy-preflight.d.ts.map +1 -1
- package/dist/commands/deploy-preflight.js +44 -5
- package/dist/commands/deploy-preflight.js.map +1 -1
- package/dist/commands/local-readiness.d.ts +9 -1
- package/dist/commands/local-readiness.d.ts.map +1 -1
- package/dist/commands/local-readiness.js +74 -4
- package/dist/commands/local-readiness.js.map +1 -1
- package/dist/commands/project-setup.d.ts.map +1 -1
- package/dist/commands/project-setup.js +10 -15
- package/dist/commands/project-setup.js.map +1 -1
- package/dist/control-plane-request.d.ts +4 -0
- package/dist/control-plane-request.d.ts.map +1 -1
- package/dist/control-plane-request.js +40 -20
- package/dist/control-plane-request.js.map +1 -1
- package/dist/project-scaffold-model.d.ts +14 -0
- package/dist/project-scaffold-model.d.ts.map +1 -0
- package/dist/project-scaffold-model.js +36 -0
- package/dist/project-scaffold-model.js.map +1 -0
- package/dist/project-scaffold-templates.d.ts.map +1 -1
- package/dist/project-scaffold-templates.js +73 -39
- package/dist/project-scaffold-templates.js.map +1 -1
- package/dist/project-scaffold-test-template.d.ts +4 -0
- package/dist/project-scaffold-test-template.d.ts.map +1 -0
- package/dist/project-scaffold-test-template.js +89 -0
- package/dist/project-scaffold-test-template.js.map +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +1 -2
- package/dist/project.js.map +1 -1
- package/dist/widget-focused-template.d.ts +1 -1
- package/dist/widget-focused-template.d.ts.map +1 -1
- package/dist/widget-focused-template.js +34 -12
- package/dist/widget-focused-template.js.map +1 -1
- package/dist/widget-scaffold-template.d.ts +1 -1
- package/dist/widget-scaffold-template.d.ts.map +1 -1
- package/dist/widget-scaffold-template.js +75 -43
- package/dist/widget-scaffold-template.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +17 -3
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +9 -12
- package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-operations-ref.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +32 -26
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +2 -1
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +6 -0
- package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +10 -4
- package/node_modules/@noodle-borg/compiler/package.json +0 -1
- package/node_modules/@noodle-borg/deploy-client/dist/deploy-payload-size.js +6 -2
- package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +193 -96
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +3 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +3 -0
- package/node_modules/@noodleseed/assistant/README.md +21 -23
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +5 -4
- package/dist/saas-scaffold-template.d.ts +0 -4
- package/dist/saas-scaffold-template.d.ts.map +0 -1
- package/dist/saas-scaffold-template.js +0 -56
- package/dist/saas-scaffold-template.js.map +0 -1
- package/node_modules/tailwindcss/LICENSE +0 -21
- package/node_modules/tailwindcss/README.md +0 -36
- package/node_modules/tailwindcss/dist/chunk-5JIJA4QV.mjs +0 -1
- package/node_modules/tailwindcss/dist/chunk-HMCCH6MG.mjs +0 -1
- package/node_modules/tailwindcss/dist/chunk-X4GG3EDV.mjs +0 -1
- package/node_modules/tailwindcss/dist/colors-C__qRT83.d.ts +0 -347
- package/node_modules/tailwindcss/dist/colors.d.mts +0 -347
- package/node_modules/tailwindcss/dist/colors.d.ts +0 -5
- package/node_modules/tailwindcss/dist/colors.js +0 -1
- package/node_modules/tailwindcss/dist/colors.mjs +0 -1
- package/node_modules/tailwindcss/dist/default-theme.d.mts +0 -1199
- package/node_modules/tailwindcss/dist/default-theme.d.ts +0 -1199
- package/node_modules/tailwindcss/dist/default-theme.js +0 -1
- package/node_modules/tailwindcss/dist/default-theme.mjs +0 -1
- package/node_modules/tailwindcss/dist/flatten-color-palette.d.mts +0 -6
- package/node_modules/tailwindcss/dist/flatten-color-palette.d.ts +0 -6
- package/node_modules/tailwindcss/dist/flatten-color-palette.js +0 -1
- package/node_modules/tailwindcss/dist/flatten-color-palette.mjs +0 -1
- package/node_modules/tailwindcss/dist/lib.d.mts +0 -379
- package/node_modules/tailwindcss/dist/lib.d.ts +0 -3
- package/node_modules/tailwindcss/dist/lib.js +0 -38
- package/node_modules/tailwindcss/dist/lib.mjs +0 -38
- package/node_modules/tailwindcss/dist/plugin.d.mts +0 -11
- package/node_modules/tailwindcss/dist/plugin.d.ts +0 -134
- package/node_modules/tailwindcss/dist/plugin.js +0 -1
- package/node_modules/tailwindcss/dist/plugin.mjs +0 -1
- package/node_modules/tailwindcss/dist/resolve-config-B4yBzhca.d.ts +0 -29
- package/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts +0 -190
- package/node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts +0 -128
- package/node_modules/tailwindcss/index.css +0 -944
- package/node_modules/tailwindcss/package.json +0 -89
- package/node_modules/tailwindcss/preflight.css +0 -393
- package/node_modules/tailwindcss/theme.css +0 -510
- package/node_modules/tailwindcss/utilities.css +0 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { embedBrowserContractTest } from './assistant-embed-tests.js';
|
|
2
|
+
/** Reuse preflight's explicit env aliases instead of duplicating browser configuration. */
|
|
3
|
+
export function embedCheckCommand(surface) {
|
|
4
|
+
const flags = surface === 'public'
|
|
5
|
+
? ' --env-alias NOODLE_SERVICE_URL=NEXT_PUBLIC_NOODLE_SERVICE_URL --require-env NEXT_PUBLIC_NOODLE_EMBED_ID'
|
|
6
|
+
: surface === 'mixed'
|
|
7
|
+
? ' --require-env NEXT_PUBLIC_NOODLE_EMBED_ID --require-env NEXT_PUBLIC_NOODLE_SERVICE_URL'
|
|
8
|
+
: '';
|
|
9
|
+
return `noodle assistant embed --check --surface ${surface}${flags} --json`;
|
|
10
|
+
}
|
|
11
|
+
/** Public mounting uses the existing hosted session source, never a customer proxy route. */
|
|
12
|
+
export function publicEmbedFiles(surface) {
|
|
13
|
+
const mixed = surface === 'mixed';
|
|
14
|
+
return {
|
|
15
|
+
'test/noodle-assistant.browser.mjs': embedBrowserContractTest(),
|
|
16
|
+
'components/noodle-assistant.tsx': `'use client';
|
|
17
|
+
|
|
18
|
+
import dynamic from 'next/dynamic';
|
|
19
|
+
${mixed ? "import { useEffect, useRef } from 'react';\n" : ''}
|
|
20
|
+
const NoodleAssistant = dynamic(
|
|
21
|
+
() => import('@noodleseed/assistant/react').then((mod) => mod.NoodleAssistant),
|
|
22
|
+
{ ssr: false },
|
|
23
|
+
);
|
|
24
|
+
${mixed
|
|
25
|
+
? `
|
|
26
|
+
export interface AssistantSignInRequest {
|
|
27
|
+
readonly signInTicket: string;
|
|
28
|
+
readonly expiresAt: string;
|
|
29
|
+
readonly intent: 'sign-in' | 'sign-up';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Bind the ticket to your backend's existing login transaction; never put it in a URL or storage. */
|
|
33
|
+
export function AssistantWidget({ principalKey, onSignInRequested }: {
|
|
34
|
+
/** Null while anonymous; change this stable user/tenant key whenever the signed-in account changes. */
|
|
35
|
+
readonly principalKey: string | null;
|
|
36
|
+
readonly onSignInRequested: (request: AssistantSignInRequest) => void;
|
|
37
|
+
}) {
|
|
38
|
+
const host = useRef<HTMLDivElement>(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const element = host.current;
|
|
41
|
+
if (!element) return;
|
|
42
|
+
const signIn = (event: Event) => {
|
|
43
|
+
if (!(event instanceof CustomEvent)) return;
|
|
44
|
+
const detail: unknown = event.detail;
|
|
45
|
+
if (typeof detail !== 'object' || detail === null ||
|
|
46
|
+
!('signInTicket' in detail) || typeof detail.signInTicket !== 'string' ||
|
|
47
|
+
!('expiresAt' in detail) || typeof detail.expiresAt !== 'string' ||
|
|
48
|
+
!('intent' in detail) || (detail.intent !== 'sign-in' && detail.intent !== 'sign-up')) return;
|
|
49
|
+
onSignInRequested({ signInTicket: detail.signInTicket, expiresAt: detail.expiresAt, intent: detail.intent });
|
|
50
|
+
};
|
|
51
|
+
element.addEventListener('assistant-sign-in-requested', signIn);
|
|
52
|
+
return () => element.removeEventListener('assistant-sign-in-requested', signIn);
|
|
53
|
+
}, [onSignInRequested]);
|
|
54
|
+
return <div ref={host}>{principalKey === null ? <NoodleAssistant
|
|
55
|
+
key="anonymous"
|
|
56
|
+
embedId={process.env.NEXT_PUBLIC_NOODLE_EMBED_ID}
|
|
57
|
+
serviceUrl={process.env.NEXT_PUBLIC_NOODLE_SERVICE_URL}
|
|
58
|
+
theme="auto"
|
|
59
|
+
/> : <NoodleAssistant
|
|
60
|
+
key={principalKey}
|
|
61
|
+
sessionEndpoint="/api/assistant/session"
|
|
62
|
+
theme="auto"
|
|
63
|
+
/>}</div>;
|
|
64
|
+
}`
|
|
65
|
+
: `
|
|
66
|
+
/** The embed ID is public configuration; the runtime owns anonymous sessions and admission. */
|
|
67
|
+
export function AssistantWidget() {
|
|
68
|
+
return <NoodleAssistant
|
|
69
|
+
embedId={process.env.NEXT_PUBLIC_NOODLE_EMBED_ID}
|
|
70
|
+
serviceUrl={process.env.NEXT_PUBLIC_NOODLE_SERVICE_URL}
|
|
71
|
+
theme="auto"
|
|
72
|
+
/>;
|
|
73
|
+
}`}
|
|
74
|
+
`,
|
|
75
|
+
'.env.local.example': `# Public configuration only; safe in the browser bundle.
|
|
76
|
+
# Use the local embed ID printed by noodle dev, or the hosted ID printed by noodle deploy.
|
|
77
|
+
NEXT_PUBLIC_NOODLE_EMBED_ID=
|
|
78
|
+
NEXT_PUBLIC_NOODLE_SERVICE_URL=https://cloud.noodleseed.dev
|
|
79
|
+
${mixed ? '' : 'PUBLIC_APP_ORIGIN=http://localhost:3000\n'}# Run checks with these names exported; the CLI does not load application dotenv files.
|
|
80
|
+
# Declare the page's exact origin in publicWebsite({ origins: [...] }).
|
|
81
|
+
# If the host sends CSP, allow the service in connect-src and frame-src.
|
|
82
|
+
# Script-tag mounts also require script-src. No customer backend config route is needed.
|
|
83
|
+
`,
|
|
84
|
+
'NOODLE-INTEGRATION.md': embedIntegrationGuide(surface),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** One customer handoff accompanies every installed profile and its machine-readable plan. */
|
|
88
|
+
export function embedIntegrationGuide(surface) {
|
|
89
|
+
return `# Noodle application integration
|
|
90
|
+
|
|
91
|
+
Surface: **${surface}**. Installation is not integration proof: application authorization, backend operations
|
|
92
|
+
and browser behavior are **unverified** until the checks below execute against your application.
|
|
93
|
+
|
|
94
|
+
## Application-owned changes
|
|
95
|
+
|
|
96
|
+
${surface === 'public'
|
|
97
|
+
? `Set the public embed ID and service URL, then render \`AssistantWidget\`. Public sessions need no customer
|
|
98
|
+
backend session/config route or backend credential. Reuse existing business endpoints for capability data.
|
|
99
|
+
Public admission and capability allowlists are enforced by the runtime; Origin alone is not authentication.`
|
|
100
|
+
: `Implement \`authenticateAssistantRequest\` in \`lib/noodle-assistant-auth.ts\` using the existing login/session
|
|
101
|
+
and backend membership checks. It returns null for signed-out callers. Derive identity, tenant, roles,
|
|
102
|
+
scopes and routing server-side; never accept them from browser JSON. Preserve your application's CSRF policy.
|
|
103
|
+
Keep backend exchange credentials in the existing server secret manager, never in browser-prefixed variables.`}
|
|
104
|
+
${surface === 'mixed'
|
|
105
|
+
? `
|
|
106
|
+
Supply \`AssistantWidget\`'s \`principalKey\` (null while anonymous, then a stable user/tenant key) and
|
|
107
|
+
\`onSignInRequested\` callback. The key resets browser state on account changes; it is not authentication.
|
|
108
|
+
Bind \`signInTicket\` to a short-lived backend login
|
|
109
|
+
transaction before using the existing login or signup flow. After authentication, spend that ticket through
|
|
110
|
+
the generated authentication adapter by returning \`signInTicket\` with the verified user; the maintained handler
|
|
111
|
+
spends it and forwards the response unchanged
|
|
112
|
+
through the destination's same-origin session endpoint. Never put a ticket in URLs, analytics, logs or durable
|
|
113
|
+
browser storage. Ticket possession is not authentication or confirmation. This application-owned transaction
|
|
114
|
+
is unverified until your login round trip is tested; do not silently replace it with a new anonymous session.
|
|
115
|
+
`
|
|
116
|
+
: ''}
|
|
117
|
+
|
|
118
|
+
Reuse the application's existing business functions and authorization. Add a thin stable HTTPS handler only
|
|
119
|
+
when no suitable API exists. Keep \`server.ts\` in the Noodle project; do not duplicate business logic or create
|
|
120
|
+
a second login system. The customer application remains the system of record.
|
|
121
|
+
|
|
122
|
+
## Verify before declaring readiness
|
|
123
|
+
|
|
124
|
+
1. Install a compatible \`@noodleseed/assistant\` version in this application's package manager and build the host.
|
|
125
|
+
${surface === 'public' ? '' : ' The server entry must export `createAssistantSessionHandler`. Run the supplied contract suite with your Vitest runner: `vitest run test/noodle-assistant.test.ts`. If the application has no Vitest dependency, add it using its existing package manager first. The suite exercises the generated route with synthetic identities; add real session/membership acceptance cases in your existing test harness.\n'}
|
|
126
|
+
2. Configure the exact origins and operator bindings; do not put environment-specific secrets in source.
|
|
127
|
+
3. With these environment names exported, run \`${embedCheckCommand(surface)}\`; this is static evidence only.
|
|
128
|
+
4. ${surface === 'public' ? 'Exercise anonymous discovery and a safe handoff, and prove private capabilities are unavailable.' : 'Test signed-out JSON 401, wrong-origin rejection, invalid JSON/content type, and successful verified identity.'}
|
|
129
|
+
5. Test cross-tenant denial at the real application boundary and an authorized read plus confirmed action or handoff.
|
|
130
|
+
6. Exercise the actual browser mount, CSP, one turn and any linked App in an authorized local or sandbox environment.
|
|
131
|
+
|
|
132
|
+
The supplied mount check runs with \`node --test test/noodle-assistant.browser.mjs\` while the application is
|
|
133
|
+
running at \`PUBLIC_APP_ORIGIN\`. It uses Playwright and Chromium; reuse the application's browser-test setup,
|
|
134
|
+
or add Playwright as a development dependency and install Chromium first. Reuse existing login fixtures for
|
|
135
|
+
protected pages. This check does not submit a turn, confirm an action or claim production readiness.
|
|
136
|
+
|
|
137
|
+
Missing credentials, a test identity, a reachable sandbox or an authorized action remain unverified. Mock tests
|
|
138
|
+
prove only their fixtures. Do not call a production mutation just to complete a check.
|
|
139
|
+
|
|
140
|
+
## Ownership and updates
|
|
141
|
+
|
|
142
|
+
These files are customer-owned after installation. Preview with \`noodle assistant embed --surface ${surface} --dry-run --json\`;
|
|
143
|
+
reruns create missing files and report conflicts without overwriting modifications. Review changes before using
|
|
144
|
+
\`--force\`. Keep custom session/business code and secrets; update library versions through the normal dependency
|
|
145
|
+
workflow and rerun the integration checks. Deployment, configuration writes and publication need their own authority.
|
|
146
|
+
|
|
147
|
+
Guide: https://docs.noodleseed.dev/docs/guides/embedded-assistant
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=assistant-embed-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-profile.js","sourceRoot":"","sources":["../src/assistant-embed-profile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,MAAM,KAAK,GACT,OAAO,KAAK,QAAQ;QAClB,CAAC,CAAC,0GAA0G;QAC5G,CAAC,CAAC,OAAO,KAAK,OAAO;YACnB,CAAC,CAAC,yFAAyF;YAC3F,CAAC,CAAC,EAAE,CAAC;IACX,OAAO,4CAA4C,OAAO,GAAG,KAAK,SAAS,CAAC;AAC9E,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,gBAAgB,CAAC,OAA2B;IAC1D,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,CAAC;IAClC,OAAO;QACL,mCAAmC,EAAE,wBAAwB,EAAE;QAC/D,iCAAiC,EAAE;;;EAGrC,KAAK,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC,CAAC,EAAE;;;;;EAM3D,KAAK;YACH,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCJ;YACE,CAAC,CAAC;;;;;;;;EASN;CACC;QACG,oBAAoB,EAAE;;;;EAIxB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,2CAA2C;;;;CAIzD;QACG,uBAAuB,EAAE,qBAAqB,CAAC,OAAO,CAAC;KACxD,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,qBAAqB,CAAC,OAAqB;IACzD,OAAO;;aAEI,OAAO;;;;;EAMlB,OAAO,KAAK,QAAQ;QAClB,CAAC,CAAC;;4GAEsG;QACxG,CAAC,CAAC;;;8GAIN;EAEE,OAAO,KAAK,OAAO;QACjB,CAAC,CAAC;;;;;;;;;;CAUL;QACG,CAAC,CAAC,EACN;;;;;;;;;EASE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,sZAAsZ;;kDAElY,iBAAiB,CAAC,OAAO,CAAC;KACvE,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,kGAAkG,CAAC,CAAC,CAAC,gHAAgH;;;;;;;;;;;;;;qGAc5I,OAAO;;;;;;CAM3G,CAAC;AACF,CAAC"}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* reconciles per file and never overwrites user edits without `--force`
|
|
6
6
|
* (docs/roadmap/embedded-assistant-hardening.md S8).
|
|
7
7
|
*/
|
|
8
|
-
export type EmbedFramework = 'nextjs';
|
|
8
|
+
export type EmbedFramework = 'nextjs' | 'django-vue';
|
|
9
|
+
export type EmbedSurface = 'authenticated' | 'public' | 'mixed';
|
|
9
10
|
/** Return the canonical files for embedding an assistant in one supported host framework. */
|
|
10
|
-
export declare function embedScaffoldFiles(framework: EmbedFramework): Record<string, string>;
|
|
11
|
+
export declare function embedScaffoldFiles(framework: EmbedFramework, surface?: EmbedSurface): Record<string, string>;
|
|
11
12
|
//# sourceMappingURL=assistant-embed-scaffold-template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-embed-scaffold-template.d.ts","sourceRoot":"","sources":["../src/assistant-embed-scaffold-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"assistant-embed-scaffold-template.d.ts","sourceRoot":"","sources":["../src/assistant-embed-scaffold-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,YAAY,CAAC;AACrD,MAAM,MAAM,YAAY,GAAG,eAAe,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhE,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,cAAc,EACzB,OAAO,GAAE,YAA8B,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqFxB"}
|
|
@@ -5,99 +5,41 @@
|
|
|
5
5
|
* reconciles per file and never overwrites user edits without `--force`
|
|
6
6
|
* (docs/roadmap/embedded-assistant-hardening.md S8).
|
|
7
7
|
*/
|
|
8
|
+
import { djangoEmbedFiles } from './assistant-embed-django.js';
|
|
9
|
+
import { embedIntegrationGuide, publicEmbedFiles } from './assistant-embed-profile.js';
|
|
10
|
+
import { embedBrowserContractTest, nextSessionContractTest } from './assistant-embed-tests.js';
|
|
8
11
|
/** Return the canonical files for embedding an assistant in one supported host framework. */
|
|
9
|
-
export function embedScaffoldFiles(framework) {
|
|
12
|
+
export function embedScaffoldFiles(framework, surface = 'authenticated') {
|
|
13
|
+
if (framework === 'django-vue') {
|
|
14
|
+
if (surface !== 'authenticated')
|
|
15
|
+
throw new Error('django-vue supports authenticated surfaces only');
|
|
16
|
+
return djangoEmbedFiles();
|
|
17
|
+
}
|
|
10
18
|
if (framework !== 'nextjs')
|
|
11
19
|
throw new Error(`unsupported framework: ${String(framework)}`);
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
if (surface === 'public')
|
|
21
|
+
return publicEmbedFiles(surface);
|
|
22
|
+
const files = {
|
|
23
|
+
'app/api/assistant/session/route.ts': `import { createAssistantSessionHandler } from '@noodleseed/assistant/server';
|
|
14
24
|
import { authenticateAssistantRequest } from '../../../../lib/noodle-assistant-auth';
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export async function POST(request: Request) {
|
|
26
|
-
const appOrigin = requireEnv('PUBLIC_APP_ORIGIN');
|
|
27
|
-
if (request.headers.get('origin') !== appOrigin) {
|
|
28
|
-
return jsonError(403, 'origin is not allowed');
|
|
29
|
-
}
|
|
30
|
-
if (request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
31
|
-
return jsonError(415, 'application/json required');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// This direct adapter call avoids auth middleware turning an API failure into an HTML login redirect.
|
|
35
|
-
const identity = await authenticateAssistantRequest(request);
|
|
36
|
-
if (identity === null) return jsonError(401, 'authentication required');
|
|
37
|
-
|
|
38
|
-
let parsed: unknown;
|
|
39
|
-
try {
|
|
40
|
-
parsed = await request.json();
|
|
41
|
-
} catch {
|
|
42
|
-
return jsonError(400, 'invalid JSON');
|
|
43
|
-
}
|
|
44
|
-
if (!isAssistantSessionRequest(parsed)) {
|
|
45
|
-
return jsonError(400, 'invalid request body');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const session = await createAssistantSession({
|
|
49
|
-
serviceUrl: requireEnv('NOODLE_SERVICE_URL'),
|
|
50
|
-
clientId: requireEnv('NOODLE_ASSISTANT_CLIENT_ID'),
|
|
51
|
-
clientSecret: requireEnv('NOODLE_ASSISTANT_CLIENT_SECRET'),
|
|
52
|
-
origin: appOrigin,
|
|
53
|
-
...identity,
|
|
54
|
-
...(parsed.context !== undefined ? { context: parsed.context } : {}),
|
|
55
|
-
});
|
|
56
|
-
// Forward the session unchanged: the widget reads endpoints/configuration from it directly.
|
|
57
|
-
return Response.json(session, { headers: { 'Cache-Control': 'no-store' } });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Accept only the bounded body that the embedding backend may forward into an assistant session. */
|
|
61
|
-
function isAssistantSessionRequest(value: unknown): value is AssistantSessionRequest {
|
|
62
|
-
if (!isRecord(value) || Object.keys(value).some((key) => key !== 'context')) return false;
|
|
63
|
-
if (value.context === undefined) return true;
|
|
64
|
-
return (
|
|
65
|
-
isRecord(value.context) &&
|
|
66
|
-
Object.values(value.context).every(
|
|
67
|
-
(entry) =>
|
|
68
|
-
entry === null ||
|
|
69
|
-
typeof entry === 'string' ||
|
|
70
|
-
typeof entry === 'number' ||
|
|
71
|
-
typeof entry === 'boolean',
|
|
72
|
-
)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Reject null and arrays before reading JSON object fields. */
|
|
77
|
-
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
78
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function requireEnv(name: string): string {
|
|
82
|
-
const value = process.env[name];
|
|
83
|
-
if (value === undefined || value === '') throw new Error(\`\${name} is not configured\`);
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function jsonError(status: number, error: string): Response {
|
|
88
|
-
return Response.json({ error }, { status, headers: { 'Cache-Control': 'no-store' } });
|
|
89
|
-
}
|
|
26
|
+
// Library-owned guards, bounded parsing and exchange. Only the existing login adapter is app-specific.
|
|
27
|
+
// Keep these credentials server-only and preserve any stricter application CSRF middleware.
|
|
28
|
+
export const POST = createAssistantSessionHandler({
|
|
29
|
+
serviceUrl: process.env.NOODLE_SERVICE_URL,
|
|
30
|
+
clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID,
|
|
31
|
+
clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET,
|
|
32
|
+
origin: process.env.PUBLIC_APP_ORIGIN,
|
|
33
|
+
authenticate: authenticateAssistantRequest,
|
|
34
|
+
});
|
|
90
35
|
`,
|
|
91
|
-
'lib/noodle-assistant-auth.ts': `import type {
|
|
36
|
+
'lib/noodle-assistant-auth.ts': `import type { AssistantSessionIdentity } from '@noodleseed/assistant/server';
|
|
92
37
|
|
|
93
38
|
/**
|
|
94
39
|
* The only application-specific adapter in the generated integration. Replace the function body with
|
|
95
40
|
* the host application's existing cookie/JWT auth and server-owned membership lookup.
|
|
96
41
|
*/
|
|
97
|
-
export type NoodleAssistantIdentity =
|
|
98
|
-
CreateAssistantSessionInput,
|
|
99
|
-
'user' | 'claims' | 'preferences' | 'routing'
|
|
100
|
-
>;
|
|
42
|
+
export type NoodleAssistantIdentity = AssistantSessionIdentity;
|
|
101
43
|
|
|
102
44
|
export async function authenticateAssistantRequest(
|
|
103
45
|
_request: Request,
|
|
@@ -105,6 +47,8 @@ export async function authenticateAssistantRequest(
|
|
|
105
47
|
// Return null for a signed-out request; the route converts it to a JSON 401. Do not redirect here.
|
|
106
48
|
// Derive roles, scopes, claims, and routing only from verified backend state. In particular,
|
|
107
49
|
// routing.endpoints must come from server-owned membership, never request JSON or headers.
|
|
50
|
+
// For mixed-mode continuation, return signInTicket only from the backend-bound login transaction;
|
|
51
|
+
// browser JSON cannot select it. The library preserves the continued session and automatic resume.
|
|
108
52
|
//
|
|
109
53
|
// The route's JSON content type plus exact-Origin check is its minimum cookie/CSRF boundary. If the
|
|
110
54
|
// host requires a CSRF token header, keep that middleware and use the DOM-free client with an injected
|
|
@@ -112,6 +56,8 @@ export async function authenticateAssistantRequest(
|
|
|
112
56
|
return null;
|
|
113
57
|
}
|
|
114
58
|
`,
|
|
59
|
+
'test/noodle-assistant.test.ts': nextSessionContractTest(),
|
|
60
|
+
'test/noodle-assistant.browser.mjs': embedBrowserContractTest(),
|
|
115
61
|
'components/noodle-assistant.tsx': `'use client';
|
|
116
62
|
|
|
117
63
|
import dynamic from 'next/dynamic';
|
|
@@ -123,8 +69,10 @@ const NoodleAssistant = dynamic(
|
|
|
123
69
|
);
|
|
124
70
|
|
|
125
71
|
/** Mount only inside the authenticated application surface. */
|
|
126
|
-
export function AssistantWidget() {
|
|
127
|
-
|
|
72
|
+
export function AssistantWidget({ principalKey }: { readonly principalKey: string }) {
|
|
73
|
+
// Stable user/tenant key from existing application state. Remount when the account changes.
|
|
74
|
+
// This key clears browser state; the backend session adapter remains the identity authority.
|
|
75
|
+
return <NoodleAssistant key={principalKey} sessionEndpoint="/api/assistant/session" theme="auto" />;
|
|
128
76
|
}
|
|
129
77
|
`,
|
|
130
78
|
'.env.local.example': `# Backend-only assistant credentials — from \`noodle assistant clients create\`.
|
|
@@ -140,5 +88,12 @@ PUBLIC_APP_ORIGIN=http://localhost:3000
|
|
|
140
88
|
# noodle assistant embed --check --json
|
|
141
89
|
`,
|
|
142
90
|
};
|
|
91
|
+
return {
|
|
92
|
+
...files,
|
|
93
|
+
...(surface === 'mixed' ? publicEmbedFiles(surface) : {}),
|
|
94
|
+
'.env.local.example': files['.env.local.example'] +
|
|
95
|
+
(surface === 'mixed' ? publicEmbedFiles(surface)['.env.local.example'] : ''),
|
|
96
|
+
'NOODLE-INTEGRATION.md': embedIntegrationGuide(surface),
|
|
97
|
+
};
|
|
143
98
|
}
|
|
144
99
|
//# sourceMappingURL=assistant-embed-scaffold-template.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-embed-scaffold-template.js","sourceRoot":"","sources":["../src/assistant-embed-scaffold-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"assistant-embed-scaffold-template.js","sourceRoot":"","sources":["../src/assistant-embed-scaffold-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACvF,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAK/F,6FAA6F;AAC7F,MAAM,UAAU,kBAAkB,CAChC,SAAyB,EACzB,UAAwB,eAAe;IAEvC,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,eAAe;YAC7B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,OAAO,gBAAgB,EAAE,CAAC;IAC5B,CAAC;IACD,IAAI,SAAS,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAC3F,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG;QACZ,oCAAoC,EAAE;;;;;;;;;;;;CAYzC;QACG,8BAA8B,EAAE;;;;;;;;;;;;;;;;;;;;;;CAsBnC;QACG,+BAA+B,EAAE,uBAAuB,EAAE;QAC1D,mCAAmC,EAAE,wBAAwB,EAAE;QAC/D,iCAAiC,EAAE;;;;;;;;;;;;;;;;CAgBtC;QACG,oBAAoB,EAAE;;;;;;;;;;;CAWzB;KACE,CAAC;IACF,OAAO;QACL,GAAG,KAAK;QACR,GAAG,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,oBAAoB,EAClB,KAAK,CAAC,oBAAoB,CAAC;YAC3B,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9E,uBAAuB,EAAE,qBAAqB,CAAC,OAAO,CAAC;KACxD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Runnable browser mount check; opening the assistant or executing a workflow is deliberately separate. */
|
|
2
|
+
export declare function embedBrowserContractTest(): string;
|
|
3
|
+
/** Executable customer-side contract tests; synthetic identities are not proof of the real login seam. */
|
|
4
|
+
export declare function nextSessionContractTest(): string;
|
|
5
|
+
//# sourceMappingURL=assistant-embed-tests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-tests.d.ts","sourceRoot":"","sources":["../src/assistant-embed-tests.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,wBAAgB,wBAAwB,IAAI,MAAM,CA2BjD;AAED,0GAA0G;AAC1G,wBAAgB,uBAAuB,IAAI,MAAM,CA0EhD"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Runnable browser mount check; opening the assistant or executing a workflow is deliberately separate. */
|
|
2
|
+
export function embedBrowserContractTest() {
|
|
3
|
+
return `import assert from 'node:assert/strict';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { chromium } from 'playwright';
|
|
6
|
+
|
|
7
|
+
test('assistant mounts in the running customer application', { timeout: 30_000 }, async () => {
|
|
8
|
+
const origin = process.env.PUBLIC_APP_ORIGIN;
|
|
9
|
+
assert.ok(origin, 'Set PUBLIC_APP_ORIGIN to the running local or approved sandbox application.');
|
|
10
|
+
assert.equal(new URL(origin).origin, origin, 'PUBLIC_APP_ORIGIN must be an exact origin, without a path.');
|
|
11
|
+
const browser = await chromium.launch({ headless: true });
|
|
12
|
+
try {
|
|
13
|
+
// If the application requires login before this page, reuse its existing login fixture here.
|
|
14
|
+
// Never commit a real cookie/token or log a session. This test never submits a turn or action.
|
|
15
|
+
const page = await browser.newPage();
|
|
16
|
+
const failures = [];
|
|
17
|
+
page.on('pageerror', () => failures.push('browser JavaScript failed'));
|
|
18
|
+
await page.goto(origin);
|
|
19
|
+
await page.waitForFunction(() => Boolean(
|
|
20
|
+
customElements.get('noodle-assistant') && document.querySelector('noodle-assistant')?.shadowRoot,
|
|
21
|
+
));
|
|
22
|
+
assert.deepEqual(failures, []);
|
|
23
|
+
assert.equal(await page.locator('noodle-assistant').count(), 1);
|
|
24
|
+
} finally { await browser.close(); }
|
|
25
|
+
});
|
|
26
|
+
// This proves mounting only. Separately prove real session/tenant rules, CSP, one read and a confirmed
|
|
27
|
+
// action or handoff, using approved sandbox data. Installed files and a mounted component are not readiness.
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
/** Executable customer-side contract tests; synthetic identities are not proof of the real login seam. */
|
|
31
|
+
export function nextSessionContractTest() {
|
|
32
|
+
return `import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
33
|
+
import { authenticateAssistantRequest } from '../lib/noodle-assistant-auth';
|
|
34
|
+
|
|
35
|
+
vi.mock('../lib/noodle-assistant-auth', () => ({ authenticateAssistantRequest: vi.fn() }));
|
|
36
|
+
|
|
37
|
+
const session = {
|
|
38
|
+
token: 'synthetic-test-session', expiresAt: '2030-01-01T00:00:00Z',
|
|
39
|
+
endpoints: { turns: 'https://cloud.example/turns', toolConfirmations: 'https://cloud.example/confirmations' },
|
|
40
|
+
};
|
|
41
|
+
const upstream = vi.fn<typeof fetch>();
|
|
42
|
+
let POST: (request: Request) => Promise<Response>;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
vi.resetModules();
|
|
46
|
+
vi.stubEnv('PUBLIC_APP_ORIGIN', 'https://app.example');
|
|
47
|
+
vi.stubEnv('NOODLE_SERVICE_URL', 'https://cloud.example');
|
|
48
|
+
vi.stubEnv('NOODLE_ASSISTANT_CLIENT_ID', 'synthetic-client');
|
|
49
|
+
vi.stubEnv('NOODLE_ASSISTANT_CLIENT_SECRET', 'synthetic-test-credential');
|
|
50
|
+
upstream.mockReset().mockImplementation(async () => Response.json(session));
|
|
51
|
+
vi.stubGlobal('fetch', upstream);
|
|
52
|
+
vi.mocked(authenticateAssistantRequest).mockReset().mockResolvedValue(null);
|
|
53
|
+
({ POST } = await import('../app/api/assistant/session/route'));
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
|
|
56
|
+
|
|
57
|
+
function request(body: unknown = {}, origin = 'https://app.example', contentType = 'application/json') {
|
|
58
|
+
return new Request('https://app.example/api/assistant/session', {
|
|
59
|
+
method: 'POST', headers: { origin, 'content-type': contentType }, body: JSON.stringify(body),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('generated assistant route (synthetic contract)', () => {
|
|
64
|
+
it('returns JSON 401, not an HTML redirect, for a signed-out request', async () => {
|
|
65
|
+
const response = await POST(request());
|
|
66
|
+
expect(response.status).toBe(401);
|
|
67
|
+
expect(response.headers.get('location')).toBeNull();
|
|
68
|
+
expect(response.headers.get('cache-control')).toBe('no-store');
|
|
69
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
70
|
+
expect(upstream).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
it('rejects wrong origins and form requests before reading the application session', async () => {
|
|
73
|
+
expect((await POST(request({}, 'https://attacker.example'))).status).toBe(403);
|
|
74
|
+
expect((await POST(request({}, 'https://app.example', 'text/plain'))).status).toBe(415);
|
|
75
|
+
expect(authenticateAssistantRequest).not.toHaveBeenCalled();
|
|
76
|
+
expect(upstream).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('does not let browser JSON replace the backend identity or tenant routes', async () => {
|
|
79
|
+
vi.mocked(authenticateAssistantRequest).mockResolvedValue({ user: { id: 'person-a', tenant: 'tenant-a' } });
|
|
80
|
+
expect((await POST(request({ user: { id: 'person-b', tenant: 'tenant-b' } }))).status).toBe(400);
|
|
81
|
+
expect((await POST(request({ routing: { endpoints: { api: 'https://attacker.example' } } }))).status).toBe(400);
|
|
82
|
+
expect(upstream).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it('exchanges only the verified identity and returns the short-lived session', async () => {
|
|
85
|
+
vi.mocked(authenticateAssistantRequest).mockResolvedValue({ user: { id: 'person-a', tenant: 'tenant-a' } });
|
|
86
|
+
const response = await POST(request({ context: { page: 'tasks' } }));
|
|
87
|
+
expect(response.status).toBe(200);
|
|
88
|
+
expect(await response.json()).toEqual(session);
|
|
89
|
+
expect(JSON.parse(String(upstream.mock.calls[0]?.[1]?.body))).toMatchObject({
|
|
90
|
+
user: { id: 'person-a', tenant: 'tenant-a' }, origin: 'https://app.example', context: { page: 'tasks' },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
it('fails closed on a cross-tenant sign-in refusal without retrying the ticket', async () => {
|
|
94
|
+
vi.mocked(authenticateAssistantRequest).mockResolvedValue({ user: { id: 'person-a' }, signInTicket: 'synthetic-bound-ticket' });
|
|
95
|
+
upstream.mockResolvedValue(Response.json({ code: 'elevation_tenant_mismatch' }, { status: 403 }));
|
|
96
|
+
const response = await POST(request());
|
|
97
|
+
expect(response.status).toBe(403);
|
|
98
|
+
expect((await response.json()).code).toBe('elevation_tenant_mismatch');
|
|
99
|
+
expect(upstream).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
// Add acceptance tests using your application's own signed-out, tenant-A and tenant-B session fixtures.
|
|
103
|
+
// These synthetic tests prove route wiring, not the membership rules inside authenticateAssistantRequest.
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=assistant-embed-tests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-tests.js","sourceRoot":"","sources":["../src/assistant-embed-tests.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;CAyBR,CAAC;AACF,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,uBAAuB;IACrC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwER,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-vue.d.ts","sourceRoot":"","sources":["../src/assistant-embed-vue.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,wBAAgB,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0EtD"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Vue stays static; complex SDK properties are set before connecting the element, including CSRF. */
|
|
2
|
+
export function vueEmbedFiles() {
|
|
3
|
+
return {
|
|
4
|
+
'src/components/NoodleAssistant.vue': `<script setup lang="ts">
|
|
5
|
+
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
6
|
+
import type { NoodleAssistantElement } from '@noodleseed/assistant';
|
|
7
|
+
import { assistantSessionFetch } from '../lib/noodle-assistant-transport';
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{ principalKey: string; csrfToken: string }>();
|
|
10
|
+
const host = ref<HTMLDivElement>();
|
|
11
|
+
let element: NoodleAssistantElement | undefined;
|
|
12
|
+
let disposed = false;
|
|
13
|
+
|
|
14
|
+
onMounted(async () => {
|
|
15
|
+
await import('@noodleseed/assistant');
|
|
16
|
+
if (disposed || !host.value) return;
|
|
17
|
+
element = document.createElement('noodle-assistant') as NoodleAssistantElement;
|
|
18
|
+
element.fetch = assistantSessionFetch(window.location.origin, () => props.csrfToken);
|
|
19
|
+
element.sessionEndpoint = '/api/assistant/session';
|
|
20
|
+
element.setAttribute('theme', 'auto');
|
|
21
|
+
host.value.append(element);
|
|
22
|
+
});
|
|
23
|
+
// This key clears browser state, never grants authority. Django re-verifies the actual session.
|
|
24
|
+
watch(() => props.principalKey, () => element?.resetSession());
|
|
25
|
+
onBeforeUnmount(() => { disposed = true; element?.remove(); element = undefined; });
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template><div ref="host" /></template>
|
|
29
|
+
`,
|
|
30
|
+
'src/lib/noodle-assistant-transport.ts': `/** Preserve Django CSRF without forwarding the token to Noodle or another origin. */
|
|
31
|
+
export function assistantSessionFetch(
|
|
32
|
+
origin: string,
|
|
33
|
+
csrfToken: () => string,
|
|
34
|
+
transport: typeof fetch = fetch,
|
|
35
|
+
): typeof fetch {
|
|
36
|
+
return (input, init) => {
|
|
37
|
+
const request = new Request(input instanceof Request ? input : new URL(input, origin), init);
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
if (url.origin === origin && url.pathname === '/api/assistant/session' && request.method === 'POST') {
|
|
40
|
+
const token = csrfToken();
|
|
41
|
+
if (!token) return Promise.reject(new Error('Application CSRF token is unavailable'));
|
|
42
|
+
request.headers.set('X-CSRFToken', token);
|
|
43
|
+
} else {
|
|
44
|
+
request.headers.delete('X-CSRFToken');
|
|
45
|
+
}
|
|
46
|
+
return transport(request);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
`,
|
|
50
|
+
'test/noodle-assistant-transport.test.ts': `import { describe, expect, it, vi } from 'vitest';
|
|
51
|
+
import { assistantSessionFetch } from '../src/lib/noodle-assistant-transport';
|
|
52
|
+
|
|
53
|
+
describe('Django session CSRF transport', () => {
|
|
54
|
+
it('adds the current X-CSRFToken only to the same-origin session POST', async () => {
|
|
55
|
+
const transport = vi.fn<typeof fetch>().mockResolvedValue(Response.json({}));
|
|
56
|
+
let token = 'synthetic-csrf-one';
|
|
57
|
+
const request = assistantSessionFetch('https://app.example', () => token, transport);
|
|
58
|
+
await request('/api/assistant/session', { method: 'POST', body: '{}' });
|
|
59
|
+
expect((transport.mock.calls[0]?.[0] as Request).headers.get('X-CSRFToken')).toBe(token);
|
|
60
|
+
token = 'synthetic-csrf-two';
|
|
61
|
+
await request('/api/assistant/session', { method: 'POST', body: '{}' });
|
|
62
|
+
expect((transport.mock.calls[1]?.[0] as Request).headers.get('X-CSRFToken')).toBe(token);
|
|
63
|
+
await request('https://cloud.example/v1/assistant/turns', { method: 'POST', headers: { 'X-CSRFToken': token } });
|
|
64
|
+
expect((transport.mock.calls[2]?.[0] as Request).headers.has('X-CSRFToken')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('fails closed before network activity without an application CSRF token', async () => {
|
|
67
|
+
const transport = vi.fn<typeof fetch>();
|
|
68
|
+
await expect(assistantSessionFetch('https://app.example', () => '', transport)(
|
|
69
|
+
'/api/assistant/session', { method: 'POST', body: '{}' },
|
|
70
|
+
)).rejects.toThrow('CSRF token');
|
|
71
|
+
expect(transport).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=assistant-embed-vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-vue.js","sourceRoot":"","sources":["../src/assistant-embed-vue.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,MAAM,UAAU,aAAa;IAC3B,OAAO;QACL,oCAAoC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBzC;QACG,uCAAuC,EAAE;;;;;;;;;;;;;;;;;;;CAmB5C;QACG,yCAAyC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwB9C;KACE,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-embed-ops.d.ts","sourceRoot":"","sources":["../../src/commands/assistant-embed-ops.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"assistant-embed-ops.d.ts","sourceRoot":"","sources":["../../src/commands/assistant-embed-ops.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAoInD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,KAAK,EAAE,cAAc,EACrB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,MAAM,CAAC,CA8HjB"}
|