@noodleseed/agent-kit 0.93.0 → 0.95.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/manifest.json +567 -359
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/claude-code/authoring-mcp-servers/references/authoring-workflow.md +5 -11
- package/skills/claude-code/authoring-mcp-servers/references/build-an-mcp-server.md +2 -2
- package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
- package/skills/claude-code/building-mcp-apps/references/build-an-mcp-app.md +2 -2
- package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/claude-code/connecting-apis-to-mcp/references/authoring-workflow.md +5 -11
- package/skills/claude-code/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
- package/skills/claude-code/creating-product-agent-guides/SKILL.md +1 -1
- package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/claude-code/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
- package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
- package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
- package/skills/claude-code/designing-mcp-products/references/authoring-workflow.md +5 -11
- package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/claude-code/embedding-mcp-assistants/references/authoring-workflow.md +5 -11
- package/skills/claude-code/embedding-mcp-assistants/references/embedded-assistant.md +35 -27
- package/skills/claude-code/examples/acme-discovery/src/server.ts +7 -0
- package/skills/claude-code/examples/acme-tasks/README.md +14 -4
- package/skills/claude-code/examples/customer-auth/README.md +3 -0
- package/skills/claude-code/examples/hello/README.md +4 -2
- package/skills/claude-code/examples/stateful-draft/README.md +115 -0
- package/skills/claude-code/examples/stateful-draft/noodle.json +5 -0
- package/skills/claude-code/examples/stateful-draft/package.json +22 -0
- package/skills/claude-code/examples/stateful-draft/site/client.js +44 -0
- package/skills/claude-code/examples/stateful-draft/site/demo.mjs +158 -0
- package/skills/claude-code/examples/stateful-draft/site/index.html +41 -0
- package/skills/claude-code/examples/stateful-draft/src/helpers.ts +6 -0
- package/skills/claude-code/examples/stateful-draft/src/server.ts +159 -0
- package/skills/claude-code/examples/stateful-draft/src/views/draft-card.tsx +152 -0
- package/skills/claude-code/examples/stateful-draft/src/views/widget-style.css +93 -0
- package/skills/claude-code/examples/stateful-draft/test/draft-card.test.tsx +88 -0
- package/skills/claude-code/examples/stateful-draft/test/server.test.ts +58 -0
- package/skills/claude-code/examples/stateful-draft/vitest.config.ts +6 -0
- package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
- package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/claude-code/references/authoring-workflow.md +5 -11
- package/skills/claude-code/references/build-an-mcp-app.md +2 -2
- package/skills/claude-code/references/build-an-mcp-server.md +2 -2
- package/skills/claude-code/references/connect-an-api.md +2 -2
- package/skills/claude-code/references/embedded-assistant.md +35 -27
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/claude-code/references/verify-and-recover.md +2 -1
- package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
- package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
- package/skills/claude-code/wrapping-existing-applications/SKILL.md +1 -1
- package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +5 -11
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +5 -11
- package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +2 -2
- package/skills/codex/building-mcp-apps/SKILL.md +1 -1
- package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +2 -2
- package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +5 -11
- package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
- package/skills/codex/creating-product-agent-guides/SKILL.md +1 -1
- package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
- package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
- package/skills/codex/designing-mcp-products/SKILL.md +1 -1
- package/skills/codex/designing-mcp-products/references/authoring-workflow.md +5 -11
- package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +5 -11
- package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +35 -27
- package/skills/codex/examples/acme-discovery/src/server.ts +7 -0
- package/skills/codex/examples/acme-tasks/README.md +14 -4
- package/skills/codex/examples/customer-auth/README.md +3 -0
- package/skills/codex/examples/hello/README.md +4 -2
- package/skills/codex/examples/stateful-draft/README.md +115 -0
- package/skills/codex/examples/stateful-draft/noodle.json +5 -0
- package/skills/codex/examples/stateful-draft/package.json +22 -0
- package/skills/codex/examples/stateful-draft/site/client.js +44 -0
- package/skills/codex/examples/stateful-draft/site/demo.mjs +158 -0
- package/skills/codex/examples/stateful-draft/site/index.html +41 -0
- package/skills/codex/examples/stateful-draft/src/helpers.ts +6 -0
- package/skills/codex/examples/stateful-draft/src/server.ts +159 -0
- package/skills/codex/examples/stateful-draft/src/views/draft-card.tsx +152 -0
- package/skills/codex/examples/stateful-draft/src/views/widget-style.css +93 -0
- package/skills/codex/examples/stateful-draft/test/draft-card.test.tsx +88 -0
- package/skills/codex/examples/stateful-draft/test/server.test.ts +58 -0
- package/skills/codex/examples/stateful-draft/vitest.config.ts +6 -0
- package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
- package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/codex/references/authoring-workflow.md +5 -11
- package/skills/codex/references/build-an-mcp-app.md +2 -2
- package/skills/codex/references/build-an-mcp-server.md +2 -2
- package/skills/codex/references/connect-an-api.md +2 -2
- package/skills/codex/references/embedded-assistant.md +35 -27
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/references/verify-and-recover.md +2 -1
- package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
- package/skills/codex/wrapping-existing-applications/SKILL.md +1 -1
- package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +5 -11
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Customer Auth - OIDC identity and customer-routed APIs
|
|
2
2
|
|
|
3
|
+
For a visitor who starts before signup, use the [Stateful Draft reference](../stateful-draft/README.md)
|
|
4
|
+
alongside this authenticated backend integration.
|
|
5
|
+
|
|
3
6
|
This curated example owns the customer/end-user authentication capability slot. It proves that a SaaS app
|
|
4
7
|
can protect an MCP endpoint with direct OIDC, retain role/scope-based tool authorization, and route ordinary
|
|
5
8
|
reads and confirmed actions to the API origin selected by the verified customer's identity provider.
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The smallest deployable Noodle app: a single `greet` tool authored in TypeScript with no
|
|
4
4
|
connectors, secrets, flows, widgets, or handoff policy. It still uses the current server options form
|
|
5
|
-
so new authors see where server-level branding belongs. Use it
|
|
6
|
-
|
|
5
|
+
so new authors see where server-level branding belongs. Use it for a local read or first deploy.
|
|
6
|
+
For a new project, `noodle init --template hello` also supplies isolated behavior tests: compilation,
|
|
7
|
+
tool discovery, an expected greeting and invalid-input rejection. Run `npm install` then `npm test`.
|
|
7
8
|
|
|
8
9
|
Protocol negotiation deliberately does not appear in `src/server.ts` or `noodle.json`. MCP versions
|
|
9
10
|
are platform-owned: the same deployed app automatically serves compatible legacy clients and modern
|
|
@@ -30,6 +31,7 @@ empty. One-shot commands write one envelope; streaming commands write NDJSON sna
|
|
|
30
31
|
terminal-failure envelopes so agents can parse each line independently.
|
|
31
32
|
|
|
32
33
|
```sh
|
|
34
|
+
noodle test examples/hello/src/server.ts --tool greet --args '{"name":"Ada"}' --json
|
|
33
35
|
noodle dev examples/hello/src/server.ts --app hello
|
|
34
36
|
noodle deploy examples/hello/src/server.ts --org acme --app hello
|
|
35
37
|
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Stateful Draft
|
|
2
|
+
|
|
3
|
+
**Owns:** The flagship for a useful brief before signup, authoritative caller state, and account continuation.
|
|
4
|
+
**Read when:** You want to compose a small conversational onboarding flow from existing platform capabilities.
|
|
5
|
+
**Do not put here:** Customer credentials, a new identity provider, or business-system records.
|
|
6
|
+
**Update when:** The reference tools, state schema, or runnable journey changes.
|
|
7
|
+
|
|
8
|
+
Start with the visitor's goal. Help them produce something useful before asking for an account. This
|
|
9
|
+
synthetic example collects a project title, audience, and desired outcome; those fields are illustrative,
|
|
10
|
+
not requirements for any particular SaaS product.
|
|
11
|
+
|
|
12
|
+
## The journey
|
|
13
|
+
|
|
14
|
+
1. The visitor describes a goal. The assistant asks only for missing information and proposes a brief.
|
|
15
|
+
2. `open_draft` opens an editable review and reads authoritative state, including its current revision.
|
|
16
|
+
3. `save_draft` saves the reviewed brief after confirmation. It sends the current revision and the complete
|
|
17
|
+
value to the state API. Failed or missing responses do not appear as successful saves.
|
|
18
|
+
4. The visitor may choose `continue_draft`. This read requires identity, so an anonymous visitor sees the
|
|
19
|
+
sign-in/signup card. Your host application completes its existing login and spends the bound ticket.
|
|
20
|
+
5. The authenticated assistant reads the same adopted draft. No project, subscription, or business record
|
|
21
|
+
is created by signing in.
|
|
22
|
+
|
|
23
|
+
The widget's Continue button requests the identity-dependent tool through the conversation. The ordinary
|
|
24
|
+
signup route should remain available on the embedding page.
|
|
25
|
+
|
|
26
|
+
## Run the reference
|
|
27
|
+
|
|
28
|
+
From this repository:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm install
|
|
32
|
+
pnpm build
|
|
33
|
+
noodle validate examples/stateful-draft/src/server.ts
|
|
34
|
+
noodle test examples/stateful-draft/src/server.ts
|
|
35
|
+
noodle dev examples/stateful-draft/src/server.ts --org demo --app stateful-draft
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The local runtime and widget preview exercise the typed tools. A complete mixed-assistant journey also
|
|
39
|
+
requires two host pages and a backend that verifies the user. The declared development origins are
|
|
40
|
+
`http://localhost:3001` for the public page and `http://localhost:3002` for the authenticated page; replace
|
|
41
|
+
these with your exact deployment origins. The assistant uses `noodleManaged()`, whose hosted availability
|
|
42
|
+
and budget belong to the operator. Local tool tests do not require a model key.
|
|
43
|
+
|
|
44
|
+
Use the SDK version declared in this example's package file. Older published SDKs may omit the state
|
|
45
|
+
adoption flag when compiling; the example's tests check that the flag reaches the compiled declaration.
|
|
46
|
+
|
|
47
|
+
Use [signup continuity](https://docs.noodleseed.dev/docs/guides/signup-continuity) for the complete host
|
|
48
|
+
integration and [customer-auth](../customer-auth/README.md) for customer-owned API authentication.
|
|
49
|
+
|
|
50
|
+
### Loopback demonstration with a hosted assistant
|
|
51
|
+
|
|
52
|
+
The included host serves the public page on port 3001 and a **simulated** account page on port 3002.
|
|
53
|
+
It binds to loopback and must not be published as a production authentication implementation.
|
|
54
|
+
After deploying this app, copy this directory outside the monorepo and run the commands below from that
|
|
55
|
+
copy. This keeps published dependencies from shadowing the monorepo's workspace SDK.
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pnpm install --ignore-workspace --lockfile=false
|
|
59
|
+
export NOODLE_EMBED_ID=<embed-id-printed-by-deploy>
|
|
60
|
+
pnpm site
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This is enough to test the anonymous conversation and saved brief. To exercise the synthetic account
|
|
64
|
+
handoff, create an assistant backend client for the same org/app/env with
|
|
65
|
+
`noodle assistant clients create --name first-brief-demo --org <org> --app <app> --env <env> --json`.
|
|
66
|
+
Set `NOODLE_ASSISTANT_CREDENTIALS_FILE` to the returned `secretFile` path and restart `pnpm site`.
|
|
67
|
+
The host consumes that private file without printing it or sending its contents to the browser.
|
|
68
|
+
Override `NOODLE_SERVICE_URL` only when deploying to a different hosted service.
|
|
69
|
+
|
|
70
|
+
The simulated signup chooses a random temporary demo identity and spends the ticket through the real
|
|
71
|
+
backend session helper. The host's login transaction lasts ten minutes; it is local process memory and
|
|
72
|
+
is lost on restart. A production integration replaces it with the customer's existing verified login and
|
|
73
|
+
login transaction. Do not copy the synthetic identity branch into a real application.
|
|
74
|
+
|
|
75
|
+
## Customer integration map
|
|
76
|
+
|
|
77
|
+
| Reference | Adaptation in the customer's application |
|
|
78
|
+
| :--- | :--- |
|
|
79
|
+
| Three-field brief | Select the smallest useful outcome and collect only its missing inputs |
|
|
80
|
+
| Public mixed surface | Mount the public embed on the unauthenticated website with an exact allowlist |
|
|
81
|
+
| Expiring `draft` handle | Keep only temporary, bounded coordination state; omit persistence if unnecessary |
|
|
82
|
+
| `continue_draft` | Trigger the existing signup/login at the point the visitor chooses an account |
|
|
83
|
+
| Host session endpoint | Verify the logged-in user, spend the bound ticket, return the SDK session response |
|
|
84
|
+
| Final business action | Add a typed connector to the existing authorized, idempotent create/update API |
|
|
85
|
+
|
|
86
|
+
The final business action belongs in the customer backend. Show the resulting record or its identifier
|
|
87
|
+
only after that API confirms success. Signup and state adoption alone are not completed onboarding.
|
|
88
|
+
Research or document parsing can be added later when they remove a demonstrated user burden; they are not
|
|
89
|
+
prerequisites for this reference.
|
|
90
|
+
|
|
91
|
+
## State and failure behavior
|
|
92
|
+
|
|
93
|
+
The draft uses caller scope, a finite 24-hour TTL, and `claimOnAuthentication: true`. Its `v2` schema
|
|
94
|
+
replaces the earlier title/stage illustration. Widget state is only a display cache. Reads, validation,
|
|
95
|
+
revision checks, expiry, and persistence belong to the runtime.
|
|
96
|
+
|
|
97
|
+
A stale edit requires an explicit reload and review before another save. Spending the single-use sign-in
|
|
98
|
+
ticket moves only opted-in state to the backend-verified account, preserving its revision and expiry.
|
|
99
|
+
Destination conflicts fail rather than merging two drafts. Abandoned or expired signup leaves the
|
|
100
|
+
anonymous state under its original limits. A 24-hour state TTL does not promise cross-device recovery or
|
|
101
|
+
that an arbitrary new anonymous visit can recover the conversation.
|
|
102
|
+
|
|
103
|
+
The save is a connector-backed side effect on a public/mixed surface, so it has `confirm: true`. Previewing
|
|
104
|
+
the brief has no side effect and needs neither signup nor confirmation. Do not add a confirmation to each
|
|
105
|
+
conversational answer or treat confirmation as proof of identity.
|
|
106
|
+
|
|
107
|
+
## Validate before a customer pilot
|
|
108
|
+
|
|
109
|
+
- Show useful value with no account and without repeating already supplied information.
|
|
110
|
+
- Save, reopen, and edit the actual record; test a stale revision and an unconfirmed save.
|
|
111
|
+
- Verify the same draft after the customer's real signup and login, including cancellation and expiry.
|
|
112
|
+
- Test an existing-account draft conflict and ensure it is not silently overwritten.
|
|
113
|
+
- Ensure signup triggers no unintended business write and account A cannot read account B's draft.
|
|
114
|
+
- Compare onboarding completion and first useful product outcome with the existing flow; count signups
|
|
115
|
+
separately. A demo is not evidence of improved conversion.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stateful-draft",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"validate": "noodle validate",
|
|
9
|
+
"dev": "noodle dev",
|
|
10
|
+
"deploy": "noodle deploy",
|
|
11
|
+
"site": "node site/demo.mjs"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@noodleseed/assistant": "^1.33.0",
|
|
15
|
+
"@vitejs/plugin-react": "latest",
|
|
16
|
+
"@noodleseed/one": "^0.154.0",
|
|
17
|
+
"react": "latest",
|
|
18
|
+
"react-dom": "latest",
|
|
19
|
+
"vite": "latest",
|
|
20
|
+
"vitest": "latest"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import '/sdk/index.js';
|
|
2
|
+
|
|
3
|
+
const config = await fetch('/demo-config').then((response) => response.json());
|
|
4
|
+
const mount = document.querySelector('#assistant');
|
|
5
|
+
const status = document.querySelector('#status');
|
|
6
|
+
if (config.authenticated && !config.signedIn) {
|
|
7
|
+
document.querySelector('#demo-login').hidden = false;
|
|
8
|
+
status.textContent =
|
|
9
|
+
'Your draft is ready to come with you. This next step simulates the product’s signup.';
|
|
10
|
+
} else {
|
|
11
|
+
const assistant = document.createElement('noodle-assistant');
|
|
12
|
+
assistant.setAttribute('theme', 'light');
|
|
13
|
+
if (config.authenticated) {
|
|
14
|
+
assistant.setAttribute('session-endpoint', '/assistant-session');
|
|
15
|
+
status.textContent =
|
|
16
|
+
'You are using a synthetic demo account. Open the chat to continue with your saved brief.';
|
|
17
|
+
} else {
|
|
18
|
+
assistant.setAttribute('embed-id', config.embedId);
|
|
19
|
+
assistant.setAttribute('service-url', config.serviceUrl);
|
|
20
|
+
status.textContent =
|
|
21
|
+
'Start without an account. Open the chat and describe what you want to achieve.';
|
|
22
|
+
}
|
|
23
|
+
mount.append(assistant);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
document.addEventListener('assistant-sign-in-requested', async (event) => {
|
|
27
|
+
status.textContent = 'Preparing your account handoff…';
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch('/start', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ signInTicket: event.detail.signInTicket }),
|
|
33
|
+
});
|
|
34
|
+
const result = await response.json();
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
status.textContent = result.error ?? 'Handoff not started';
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
window.location.assign(result.destination);
|
|
40
|
+
} catch {
|
|
41
|
+
status.textContent =
|
|
42
|
+
'The handoff did not start. Your brief has not been moved. Please try again.';
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Loopback-only synthetic host. Replace its demo identity with your existing login in a real app. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createAssistantSession } from '@noodleseed/assistant/server';
|
|
8
|
+
|
|
9
|
+
const publicOrigin = 'http://localhost:3001';
|
|
10
|
+
const appOrigin = 'http://localhost:3002';
|
|
11
|
+
const serviceUrl = process.env.NOODLE_SERVICE_URL ?? 'https://cloud.noodleseed.dev';
|
|
12
|
+
const embedId = process.env.NOODLE_EMBED_ID;
|
|
13
|
+
const credentialFile = process.env.NOODLE_ASSISTANT_CREDENTIALS_FILE;
|
|
14
|
+
if (!embedId?.match(/^pub_[a-z0-9]+$/i)) {
|
|
15
|
+
throw new Error('Set NOODLE_EMBED_ID from the hosted deployment.');
|
|
16
|
+
}
|
|
17
|
+
if (new URL(serviceUrl).protocol !== 'https:')
|
|
18
|
+
throw new Error('The hosted service must use HTTPS.');
|
|
19
|
+
// Consume only the explicitly supplied backend credential file. Its content is never sent to the browser.
|
|
20
|
+
const credentials = credentialFile ? JSON.parse(await readFile(credentialFile, 'utf8')) : undefined;
|
|
21
|
+
if (
|
|
22
|
+
credentials &&
|
|
23
|
+
(typeof credentials.clientId !== 'string' || typeof credentials.clientSecret !== 'string')
|
|
24
|
+
) {
|
|
25
|
+
throw new Error('The backend credential file is not a CLI-created assistant client file.');
|
|
26
|
+
}
|
|
27
|
+
const sdkDir = dirname(fileURLToPath(import.meta.resolve('@noodleseed/assistant')));
|
|
28
|
+
const siteDir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const transactions = new Map();
|
|
30
|
+
const lifetime = 10 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
function send(res, status, value, type = 'application/json') {
|
|
33
|
+
res.writeHead(status, {
|
|
34
|
+
'Content-Type': type,
|
|
35
|
+
'Cache-Control': 'no-store',
|
|
36
|
+
'Referrer-Policy': 'no-referrer',
|
|
37
|
+
'X-Content-Type-Options': 'nosniff',
|
|
38
|
+
});
|
|
39
|
+
res.end(type === 'application/json' ? JSON.stringify(value) : value);
|
|
40
|
+
}
|
|
41
|
+
function redirect(res, destination) {
|
|
42
|
+
res.writeHead(303, { Location: destination, 'Cache-Control': 'no-store' });
|
|
43
|
+
res.end();
|
|
44
|
+
}
|
|
45
|
+
function current(req) {
|
|
46
|
+
const id = /(?:^|;\s*)brief_demo=([a-f0-9-]+)/.exec(req.headers.cookie ?? '')?.[1];
|
|
47
|
+
return id ? transactions.get(id) : undefined;
|
|
48
|
+
}
|
|
49
|
+
async function jsonBody(req) {
|
|
50
|
+
let text = '';
|
|
51
|
+
for await (const chunk of req) {
|
|
52
|
+
text += chunk.toString();
|
|
53
|
+
if (Buffer.byteLength(text) > 8192) throw new Error('Request too large');
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handler(origin) {
|
|
59
|
+
return async (req, res) => {
|
|
60
|
+
// The fixed loopback host is intentional: never deploy this synthetic-login server to the internet.
|
|
61
|
+
if (req.headers.host !== new URL(origin).host) return send(res, 403, { error: 'Wrong host' });
|
|
62
|
+
for (const [id, transaction] of transactions) {
|
|
63
|
+
if (transaction.expiresAt <= Date.now()) transactions.delete(id);
|
|
64
|
+
}
|
|
65
|
+
const path = new URL(req.url ?? '/', origin).pathname;
|
|
66
|
+
try {
|
|
67
|
+
if (req.method === 'GET' && path === '/') {
|
|
68
|
+
return send(
|
|
69
|
+
res,
|
|
70
|
+
200,
|
|
71
|
+
await readFile(join(siteDir, 'index.html')),
|
|
72
|
+
'text/html; charset=utf-8',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (req.method === 'GET' && path === '/demo-config') {
|
|
76
|
+
return send(res, 200, {
|
|
77
|
+
serviceUrl,
|
|
78
|
+
embedId,
|
|
79
|
+
authenticated: origin === appOrigin,
|
|
80
|
+
signedIn: Boolean(current(req)?.user),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (req.method === 'GET' && path === '/client.js') {
|
|
84
|
+
return send(res, 200, await readFile(join(siteDir, 'client.js')), 'text/javascript');
|
|
85
|
+
}
|
|
86
|
+
if (req.method === 'GET' && /^\/sdk\/[a-z0-9._-]+\.js$/i.test(path)) {
|
|
87
|
+
return send(res, 200, await readFile(join(sdkDir, path.slice(5))), 'text/javascript');
|
|
88
|
+
}
|
|
89
|
+
if (req.method === 'POST') {
|
|
90
|
+
if (req.headers.origin !== origin) return send(res, 403, { error: 'Wrong origin' });
|
|
91
|
+
if (origin === publicOrigin && path === '/start') {
|
|
92
|
+
if (!credentials)
|
|
93
|
+
return send(res, 503, {
|
|
94
|
+
error:
|
|
95
|
+
'Signup demonstration needs an authorized backend client. The anonymous brief is still available.',
|
|
96
|
+
});
|
|
97
|
+
if (transactions.size >= 40) return send(res, 429, { error: 'Please try again later' });
|
|
98
|
+
const body = await jsonBody(req);
|
|
99
|
+
if (typeof body.signInTicket !== 'string' || body.signInTicket.length > 4096) {
|
|
100
|
+
return send(res, 400, { error: 'Missing continuation ticket' });
|
|
101
|
+
}
|
|
102
|
+
const id = randomUUID();
|
|
103
|
+
transactions.set(id, {
|
|
104
|
+
signInTicket: body.signInTicket,
|
|
105
|
+
expiresAt: Date.now() + lifetime,
|
|
106
|
+
});
|
|
107
|
+
res.setHeader(
|
|
108
|
+
'Set-Cookie',
|
|
109
|
+
`brief_demo=${id}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`,
|
|
110
|
+
);
|
|
111
|
+
return send(res, 200, { destination: appOrigin });
|
|
112
|
+
}
|
|
113
|
+
if (origin === appOrigin && path === '/demo-login') {
|
|
114
|
+
const transaction = current(req);
|
|
115
|
+
if (!transaction)
|
|
116
|
+
return send(res, 410, { error: 'Demo expired. Start again on the public page.' });
|
|
117
|
+
// Synthetic identity only. A production route must verify its existing application session.
|
|
118
|
+
transaction.user ??= { id: `demo_${randomUUID()}`, name: 'Demo visitor' };
|
|
119
|
+
return redirect(res, '/');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (origin === appOrigin && req.method === 'GET' && path === '/assistant-session') {
|
|
123
|
+
if (!credentials) return send(res, 503, { error: 'Configure the backend client first' });
|
|
124
|
+
if (req.headers['sec-fetch-site'] !== 'same-origin')
|
|
125
|
+
return send(res, 403, { error: 'Same-origin request required' });
|
|
126
|
+
const transaction = current(req);
|
|
127
|
+
if (!transaction?.user)
|
|
128
|
+
return send(res, 401, { error: 'Choose the synthetic demo account first' });
|
|
129
|
+
// Reuse the in-flight result so concurrent mounts cannot spend the single-use ticket twice.
|
|
130
|
+
transaction.session ??= createAssistantSession({
|
|
131
|
+
serviceUrl,
|
|
132
|
+
clientId: credentials.clientId,
|
|
133
|
+
clientSecret: credentials.clientSecret,
|
|
134
|
+
origin: appOrigin,
|
|
135
|
+
user: transaction.user,
|
|
136
|
+
signInTicket: transaction.signInTicket,
|
|
137
|
+
});
|
|
138
|
+
const session = await transaction.session;
|
|
139
|
+
delete transaction.signInTicket;
|
|
140
|
+
return send(res, 200, session);
|
|
141
|
+
}
|
|
142
|
+
return send(res, 404, { error: 'Not found' });
|
|
143
|
+
} catch {
|
|
144
|
+
// Do not log credentials, tickets, requests, or raw provider errors.
|
|
145
|
+
return send(res, 409, {
|
|
146
|
+
error: 'Continuation was not completed. Return to the public page and start again.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
for (const [port, origin] of [
|
|
152
|
+
[3001, publicOrigin],
|
|
153
|
+
[3002, appOrigin],
|
|
154
|
+
]) {
|
|
155
|
+
createServer(handler(origin)).listen(port, '127.0.0.1');
|
|
156
|
+
}
|
|
157
|
+
console.log(`Synthetic onboarding demo: ${publicOrigin}`);
|
|
158
|
+
console.log('Loopback only. Uses the hosted assistant; demo signup is not a real account.');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>First Brief · Conversational onboarding demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { font-family: system-ui, sans-serif; color: #172033; background: #f6f8fc; }
|
|
9
|
+
body { margin: 0; }
|
|
10
|
+
main { max-width: 880px; margin: 0 auto; padding: 64px 28px; }
|
|
11
|
+
.label { font-size: 12px; font-weight: 700; letter-spacing: .14em; color: #526077; }
|
|
12
|
+
h1 { max-width: 640px; font-size: clamp(36px, 6vw, 64px); line-height: 1.08; letter-spacing: -.055em; }
|
|
13
|
+
p { max-width: 570px; line-height: 1.6; color: #526077; }
|
|
14
|
+
.sample { padding: 24px; border: 1px solid #dce3ee; border-radius: 16px; background: white; margin: 28px 0; max-width: 550px; }
|
|
15
|
+
a { color: #174bbd; }
|
|
16
|
+
button { border: none; background: #2563eb; color: white; padding: 14px 20px; border-radius: 9px; font: inherit; cursor: pointer; }
|
|
17
|
+
:focus-visible { outline: 3px solid #2563eb; outline-offset: 4px; }
|
|
18
|
+
.notice { font-size: 13px; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<main>
|
|
23
|
+
<span class="label">FIRST BRIEF · INTERNAL DEMONSTRATION</span>
|
|
24
|
+
<h1>Make something useful.<br>Then decide to sign up.</h1>
|
|
25
|
+
<p>Describe a goal. Refine a short brief. See the result before deciding whether to take it into an account.</p>
|
|
26
|
+
<div class="sample">
|
|
27
|
+
<strong>Try this in the chat</strong>
|
|
28
|
+
<p>“I want to help new teammates complete their first useful project in a week.”</p>
|
|
29
|
+
<p id="status" role="status" aria-live="polite">Connecting…</p>
|
|
30
|
+
</div>
|
|
31
|
+
<form id="demo-login" action="/demo-login" method="post" hidden>
|
|
32
|
+
<p><strong>Simulated signup</strong><br>This creates a temporary demo identity. It does not register an account with any SaaS product.</p>
|
|
33
|
+
<button type="submit">Continue as a demo user</button>
|
|
34
|
+
<p><a href="http://localhost:3001">Cancel and return to the public page</a></p>
|
|
35
|
+
</form>
|
|
36
|
+
<p class="notice">Use synthetic information only. The assistant and draft storage are hosted by Noodle Seed. Signup is simulated locally; no customer backend is connected. Drafts expire within 24 hours. <a href="https://noodleseed.com/privacy" target="_blank" rel="noreferrer">Privacy</a></p>
|
|
37
|
+
</main>
|
|
38
|
+
<div id="assistant"></div>
|
|
39
|
+
<script type="module" src="/client.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotations,
|
|
3
|
+
authenticatedWebsite,
|
|
4
|
+
connector,
|
|
5
|
+
embeddedAssistant,
|
|
6
|
+
noodleManaged,
|
|
7
|
+
publicWebsite,
|
|
8
|
+
server,
|
|
9
|
+
tool,
|
|
10
|
+
z,
|
|
11
|
+
} from '@noodleseed/one';
|
|
12
|
+
|
|
13
|
+
const draft = z.object({
|
|
14
|
+
title: z.string().trim().min(1).max(120).optional(),
|
|
15
|
+
audience: z.string().trim().min(1).max(240).optional(),
|
|
16
|
+
goal: z.string().trim().min(1).max(240).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const snapshot = z.object({
|
|
20
|
+
value: draft,
|
|
21
|
+
revision: z.number().int(),
|
|
22
|
+
status: z.string(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const state = connector('noodle_state')
|
|
26
|
+
.version('1.0.0')
|
|
27
|
+
.operation('read_state', {
|
|
28
|
+
type: 'read',
|
|
29
|
+
input: z.object({ handle: z.string() }),
|
|
30
|
+
output: snapshot,
|
|
31
|
+
})
|
|
32
|
+
.operation('patch_state', {
|
|
33
|
+
type: 'action',
|
|
34
|
+
input: z.object({
|
|
35
|
+
handle: z.string(),
|
|
36
|
+
expectedRevision: z.number().int().min(0),
|
|
37
|
+
value: draft,
|
|
38
|
+
}),
|
|
39
|
+
output: snapshot,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const openDraft = tool('open_draft', {
|
|
43
|
+
title: 'Review your brief',
|
|
44
|
+
description:
|
|
45
|
+
'Preview a proposed brief and read the saved brief with its revision. Pass the proposed title, audience, and goal in proposal to prefill the editable preview without saving it. Omit proposal to reload the saved record after a conflict.',
|
|
46
|
+
input: z.object({ proposal: draft.default({}) }),
|
|
47
|
+
output: snapshot.extend({ proposal: draft }),
|
|
48
|
+
fulfil: ({ input, connectors }) => {
|
|
49
|
+
const saved = connectors.state.readState({ handle: 'draft' });
|
|
50
|
+
return {
|
|
51
|
+
value: saved.value,
|
|
52
|
+
revision: saved.revision,
|
|
53
|
+
status: saved.status,
|
|
54
|
+
proposal: input.proposal,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
viewTitle: 'Your project brief',
|
|
58
|
+
invoking: 'Loading your saved brief…',
|
|
59
|
+
invoked: 'Your brief is ready to review',
|
|
60
|
+
view: { component: 'draft-card', entry: './views/draft-card.tsx' },
|
|
61
|
+
annotations: annotations.readOnly(),
|
|
62
|
+
viewDescription: 'Review and edit a saved brief before deciding whether to create an account.',
|
|
63
|
+
csp: { connectDomains: [], resourceDomains: [], frameDomains: [] },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const saveDraft = tool('save_draft', {
|
|
67
|
+
title: 'Save your brief',
|
|
68
|
+
description:
|
|
69
|
+
'Save this reviewed brief for up to 24 hours. This does not create an account or a project.',
|
|
70
|
+
input: draft.required().extend({
|
|
71
|
+
expectedRevision: z.number().int().min(0).meta({ title: 'Saved version' }),
|
|
72
|
+
}),
|
|
73
|
+
output: snapshot,
|
|
74
|
+
annotations: annotations.localAction({ destructive: false, confirm: true }),
|
|
75
|
+
fulfil: ({ input, connectors }) => {
|
|
76
|
+
const saved = connectors.state.patchState({
|
|
77
|
+
handle: 'draft',
|
|
78
|
+
expectedRevision: input.expectedRevision,
|
|
79
|
+
value: { title: input.title, audience: input.audience, goal: input.goal },
|
|
80
|
+
});
|
|
81
|
+
return { value: saved.value, revision: saved.revision, status: saved.status };
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const continueDraft = tool('continue_draft', {
|
|
86
|
+
title: 'Continue with an account',
|
|
87
|
+
description:
|
|
88
|
+
'Read the brief in the signed-in account. Call only when the visitor chooses to continue with an account, after showing useful value. Sign-in carries the saved draft forward; it does not submit, publish, or create a project.',
|
|
89
|
+
input: z.object({}),
|
|
90
|
+
output: snapshot.extend({ accountId: z.string() }),
|
|
91
|
+
annotations: annotations.readOnly(),
|
|
92
|
+
fulfil: ({ connectors, user }) => {
|
|
93
|
+
const saved = connectors.state.readState({ handle: 'draft' });
|
|
94
|
+
return {
|
|
95
|
+
value: saved.value,
|
|
96
|
+
revision: saved.revision,
|
|
97
|
+
status: saved.status,
|
|
98
|
+
accountId: user.id,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export default server(
|
|
104
|
+
'stateful_draft',
|
|
105
|
+
{
|
|
106
|
+
title: 'Your first useful brief',
|
|
107
|
+
version: '1.0.0',
|
|
108
|
+
instructions:
|
|
109
|
+
'Help someone turn their goal into a concise project brief: a title, audience, and desired outcome. Base the brief on information they supply. Propose a sensible editable title; never require a naming question. If the audience and outcome are already clear, immediately show the complete proposed brief. Otherwise ask one short question for missing information and skip anything already answered. Show the useful proposed brief in chat before offering to save it. Do not request an email, password, document, or external research. Read open_draft before save_draft. Saving requires review and confirmation; an unconfirmed save is not persisted. Never claim an account or project was created. After saving, offer continue_draft only as an optional next step; never require signup to see the brief. This is a synthetic onboarding example, not an integration with a specific SaaS product.',
|
|
110
|
+
branding: {
|
|
111
|
+
name: 'First Brief',
|
|
112
|
+
accent: '#2563EB',
|
|
113
|
+
surface: '#F8FAFC',
|
|
114
|
+
surfaceDark: '#111827',
|
|
115
|
+
radius: 'md',
|
|
116
|
+
density: 'comfortable',
|
|
117
|
+
typography: 'system',
|
|
118
|
+
colorScheme: 'auto',
|
|
119
|
+
},
|
|
120
|
+
use: { state },
|
|
121
|
+
state: {
|
|
122
|
+
handles: {
|
|
123
|
+
draft: {
|
|
124
|
+
kind: 'draft',
|
|
125
|
+
version: 'v2',
|
|
126
|
+
scope: 'caller',
|
|
127
|
+
ttlSeconds: 86400,
|
|
128
|
+
claimOnAuthentication: true,
|
|
129
|
+
schema: draft,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
assistant: embeddedAssistant({
|
|
134
|
+
model: noodleManaged(),
|
|
135
|
+
privacyUrl: 'https://noodleseed.com/privacy',
|
|
136
|
+
behavior: { showConfirmationDetails: false },
|
|
137
|
+
access: [
|
|
138
|
+
publicWebsite({
|
|
139
|
+
origins: ['http://localhost:3001'],
|
|
140
|
+
capabilities: [openDraft, saveDraft, continueDraft],
|
|
141
|
+
signIn: true,
|
|
142
|
+
}),
|
|
143
|
+
authenticatedWebsite({
|
|
144
|
+
origins: ['http://localhost:3002'],
|
|
145
|
+
capabilities: [openDraft, saveDraft, continueDraft],
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
labels: {
|
|
149
|
+
welcomeHeading: 'What would you like to achieve?',
|
|
150
|
+
composerPlaceholder: 'Describe your goal…',
|
|
151
|
+
signInHeading: 'Take your brief with you',
|
|
152
|
+
signInBody: 'Continue with an account to use your saved brief inside the product.',
|
|
153
|
+
signInAction: 'Sign in',
|
|
154
|
+
signUpAction: 'Create account',
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
[openDraft, saveDraft, continueDraft],
|
|
159
|
+
);
|