@noodleseed/one 0.67.0 → 0.69.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/dist/agents.d.ts +10 -4
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +82 -26
- package/dist/agents.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/analytics-ops.d.ts +3 -1
- package/dist/commands/analytics-ops.d.ts.map +1 -1
- package/dist/commands/analytics-ops.js +31 -5
- package/dist/commands/analytics-ops.js.map +1 -1
- package/dist/commands/catalog-data-resources.d.ts.map +1 -1
- package/dist/commands/catalog-data-resources.js +10 -0
- package/dist/commands/catalog-data-resources.js.map +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +1 -0
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/envs-ops.d.ts +1 -0
- package/dist/commands/envs-ops.d.ts.map +1 -1
- package/dist/commands/envs-ops.js +49 -1
- package/dist/commands/envs-ops.js.map +1 -1
- package/dist/commands/project-setup.d.ts +3 -2
- package/dist/commands/project-setup.d.ts.map +1 -1
- package/dist/commands/project-setup.js +5 -5
- package/dist/commands/project-setup.js.map +1 -1
- package/dist/commands/resource-shared.d.ts +11 -0
- package/dist/commands/resource-shared.d.ts.map +1 -1
- package/dist/commands/resource-shared.js.map +1 -1
- package/dist/deploy.js +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/skills-update.d.ts +7 -0
- package/dist/skills-update.d.ts.map +1 -1
- package/dist/skills-update.js +9 -3
- package/dist/skills-update.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +9 -2
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +10 -0
- package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/index.js +8 -13
- package/node_modules/@noodle-borg/agent-kit/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.js +4 -3
- package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.d.ts +2 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.d.ts.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.js +59 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.js.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +7 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js +60 -20
- package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-content.d.ts +1 -8
- package/node_modules/@noodle-borg/agent-kit/dist/skill-content.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js +14 -85
- package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-design-refs.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-design-refs.js +2 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-design-refs.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.d.ts +2 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.d.ts.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.js +114 -150
- package/node_modules/@noodle-borg/agent-kit/dist/skill-operations-refs.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.d.ts +17 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.d.ts.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js +160 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.d.ts +2 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.d.ts.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.js +61 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.js.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.d.ts +2 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.d.ts.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +72 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js.map +1 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/connection-types.d.ts +52 -0
- package/node_modules/@noodle-borg/authoring/dist/connection-types.d.ts.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/connection-types.js +35 -0
- package/node_modules/@noodle-borg/authoring/dist/connection-types.js.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/connections.d.ts +26 -0
- package/node_modules/@noodle-borg/authoring/dist/connections.d.ts.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/connections.js +63 -0
- package/node_modules/@noodle-borg/authoring/dist/connections.js.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +18 -7
- package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/connectors.js +26 -4
- package/node_modules/@noodle-borg/authoring/dist/connectors.js.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/gmail.d.ts +7 -0
- package/node_modules/@noodle-borg/authoring/dist/gmail.d.ts.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/gmail.js +257 -0
- package/node_modules/@noodle-borg/authoring/dist/gmail.js.map +1 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts +2 -0
- package/node_modules/@noodle-borg/authoring/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/index.js +2 -0
- package/node_modules/@noodle-borg/authoring/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/platform.d.ts +1 -1
- package/node_modules/@noodle-borg/authoring/dist/react/components.d.ts +2 -2
- package/node_modules/@noodle-borg/authoring/dist/react/native-components.d.ts +2 -2
- package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +2 -0
- package/node_modules/@noodle-borg/authoring/dist/recording.d.ts.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/recording.js +6 -0
- package/node_modules/@noodle-borg/authoring/dist/recording.js.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/server.d.ts.map +1 -1
- package/node_modules/@noodle-borg/authoring/dist/server.js +4 -1
- package/node_modules/@noodle-borg/authoring/dist/server.js.map +1 -1
- package/node_modules/@noodle-borg/authoring/package.json +1 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +43 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.js +4 -1
- package/node_modules/@noodle-borg/compiler/dist/artifact/types.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts +15 -0
- package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/compile.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/compile.js +3 -1
- package/node_modules/@noodle-borg/compiler/dist/compile.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/confirmation-flow.d.ts +3 -3
- package/node_modules/@noodle-borg/compiler/dist/confirmation-flow.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/confirmation-flow.js +16 -17
- package/node_modules/@noodle-borg/compiler/dist/confirmation-flow.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/context-compile.d.ts +2 -4
- package/node_modules/@noodle-borg/compiler/dist/context-compile.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/context-compile.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
- package/node_modules/@noodle-borg/compiler/dist/errors.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.d.ts +17 -5
- package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +89 -0
- package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts +3 -2
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/index.js +1 -0
- package/node_modules/@noodle-borg/compiler/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +66 -16
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts.map +1 -1
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +47 -4
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/compile.js +3 -0
- package/node_modules/@noodle-borg/connector-defs/dist/compile.js.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/credential-profiles.d.ts +6 -0
- package/node_modules/@noodle-borg/connector-defs/dist/credential-profiles.d.ts.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/credential-profiles.js +25 -0
- package/node_modules/@noodle-borg/connector-defs/dist/credential-profiles.js.map +1 -0
- package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +89 -0
- package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts.map +1 -1
- package/node_modules/@noodle-borg/connector-defs/dist/schema.js +42 -1
- package/node_modules/@noodle-borg/connector-defs/dist/schema.js.map +1 -1
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts.map +1 -1
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +5 -4
- package/node_modules/@noodle-borg/connector-http/dist/http-connector.js.map +1 -1
- package/node_modules/@noodle-borg/external-credential-provider/README.md +18 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/assertion.d.ts +36 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/assertion.d.ts.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/assertion.js +72 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/assertion.js.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/contract.d.ts +82 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/contract.d.ts.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/contract.js +126 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/contract.js.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/index.d.ts +4 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/index.js +4 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/index.js.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/provider.d.ts +15 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/provider.d.ts.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/provider.js +24 -0
- package/node_modules/@noodle-borg/external-credential-provider/dist/provider.js.map +1 -0
- package/node_modules/@noodle-borg/external-credential-provider/package.json +33 -0
- package/node_modules/@noodle-borg/module/dist/contract.d.ts +14 -0
- package/node_modules/@noodle-borg/module/dist/contract.d.ts.map +1 -1
- package/node_modules/@noodle-borg/module/dist/contract.js.map +1 -1
- package/node_modules/@noodle-borg/protocol/dist/tool-interaction.d.ts.map +1 -1
- package/node_modules/@noodle-borg/protocol/dist/tool-interaction.js +1 -8
- package/node_modules/@noodle-borg/protocol/dist/tool-interaction.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/broker/map.d.ts +8 -4
- package/node_modules/@noodle-borg/runtime/dist/broker/map.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/broker/map.js +35 -5
- package/node_modules/@noodle-borg/runtime/dist/broker/map.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/broker/types.d.ts +34 -1
- package/node_modules/@noodle-borg/runtime/dist/broker/types.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/broker/types.js +38 -0
- package/node_modules/@noodle-borg/runtime/dist/broker/types.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/confirmation-analysis.d.ts +28 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-analysis.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-analysis.js +143 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-analysis.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-continuation.d.ts +6 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-continuation.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-continuation.js +56 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation-continuation.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/confirmation.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/confirmation.js +71 -74
- package/node_modules/@noodle-borg/runtime/dist/confirmation.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +3 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/types.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.d.ts +8 -0
- package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.d.ts.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.js +89 -0
- package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.js.map +1 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +2 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/execute.js +24 -0
- package/node_modules/@noodle-borg/runtime/dist/execute.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/index.js +1 -1
- package/node_modules/@noodle-borg/runtime/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts +8 -0
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts.map +1 -1
- package/node_modules/@noodle-borg/runtime/dist/result.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/credential-binding-index.d.ts +15 -0
- package/node_modules/@noodle-borg/service/dist/credential-binding-index.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/credential-binding-index.js +66 -0
- package/node_modules/@noodle-borg/service/dist/credential-binding-index.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker-options.d.ts +13 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker-options.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker-options.js +26 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker-options.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker.d.ts +5 -0
- package/node_modules/@noodle-borg/service/dist/credential-broker.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/credential-broker.js +43 -45
- package/node_modules/@noodle-borg/service/dist/credential-broker.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/credential-probes.d.ts +13 -0
- package/node_modules/@noodle-borg/service/dist/credential-probes.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/credential-probes.js +88 -0
- package/node_modules/@noodle-borg/service/dist/credential-probes.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-binding-broker.d.ts +12 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-binding-broker.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-binding-broker.js +111 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-binding-broker.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-exchange.d.ts +82 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-exchange.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-exchange.js +184 -0
- package/node_modules/@noodle-borg/service/dist/external-credential-exchange.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/service/dist/index.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/index.js +1 -0
- package/node_modules/@noodle-borg/service/dist/index.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/registry-state.d.ts +3 -1
- package/node_modules/@noodle-borg/service/dist/registry-state.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/registry-state.js +27 -2
- package/node_modules/@noodle-borg/service/dist/registry-state.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +7 -0
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/registry.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/registry.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/registry.js +23 -27
- package/node_modules/@noodle-borg/service/dist/registry.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/contracts.d.ts +16 -0
- package/node_modules/@noodle-borg/service/dist/routes/contracts.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/contracts.js +16 -0
- package/node_modules/@noodle-borg/service/dist/routes/contracts.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/environment-mutation-dispatch.d.ts +19 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-mutation-dispatch.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-mutation-dispatch.js +16 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-mutation-dispatch.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-production.d.ts +8 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-production.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-production.js +46 -0
- package/node_modules/@noodle-borg/service/dist/routes/environment-production.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/paths.d.ts +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/paths.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/paths.js +3 -0
- package/node_modules/@noodle-borg/service/dist/routes/paths.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/serve-options.d.ts +8 -0
- package/node_modules/@noodle-borg/service/dist/serve-options.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/serve.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/serve.js +3 -0
- package/node_modules/@noodle-borg/service/dist/serve.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/service.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/service.js +13 -0
- package/node_modules/@noodle-borg/service/dist/service.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/environment-production.d.ts +9 -0
- package/node_modules/@noodle-borg/service/dist/store/environment-production.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/environment-production.js +2 -0
- package/node_modules/@noodle-borg/service/dist/store/environment-production.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/in-memory.js +31 -2
- package/node_modules/@noodle-borg/service/dist/store/in-memory.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/json-file.js +64 -2
- package/node_modules/@noodle-borg/service/dist/store/json-file.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres-envs.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres-envs.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres-envs.js +53 -7
- package/node_modules/@noodle-borg/service/dist/store/postgres-envs.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres-schema.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres-schema.js +48 -0
- package/node_modules/@noodle-borg/service/dist/store/postgres-schema.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts +2 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/postgres.js +8 -2
- package/node_modules/@noodle-borg/service/dist/store/postgres.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/records.d.ts +5 -2
- package/node_modules/@noodle-borg/service/dist/store/records.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store/records.js +18 -4
- package/node_modules/@noodle-borg/service/dist/store/records.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store.d.ts +8 -2
- package/node_modules/@noodle-borg/service/dist/store.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/store.js.map +1 -1
- package/node_modules/@noodle-borg/service/package.json +1 -0
- package/package.json +2 -1
|
@@ -10,10 +10,11 @@ export const BUNDLED_EXAMPLE_NAMES = [
|
|
|
10
10
|
"acme-tasks",
|
|
11
11
|
"acme-bistro",
|
|
12
12
|
"customer-auth",
|
|
13
|
+
"gmail-multi-account",
|
|
13
14
|
];
|
|
14
15
|
/** Real example sources vendored from `examples/<name>`, written under `examples/` in the skill tree. */
|
|
15
16
|
export const BUNDLED_EXAMPLE_FILES = [
|
|
16
|
-
{ relPath: "examples/acme-bistro/README.md", content: "# Acme Bistro — end-to-end ordering with payment-only handoff\n\nA Noodle MCP App for **Acme Bistro**, a fictional restaurant. It is the flagship for the **end-to-end\ntransaction** pattern: the customer browses the menu and builds the order *in chat*, and the order\ncompletes in chat — only **payment** hands off, via a signed checkout deep link (the card never touches\nthe app). It pairs a view-backed `tool` menu/cart with app-only `tool` cart helpers and a model-visible\n`create_checkout` tool backed by `handoff.allowedDomains`.\n\nCapability slot: **end-to-end in-chat transaction + payment-only handoff**, plus a worked **design-first**\ndeliverable set (`design/` — a UX Document, a single-file HTML wireframe with an embedded OpenAI Apps SDK\ncompliance audit, and a Recommended API contract). It sets the quality bar the `noodle-seed` skill's\n`references/experience-design.md` and `references/
|
|
17
|
+
{ relPath: "examples/acme-bistro/README.md", content: "# Acme Bistro — end-to-end ordering with payment-only handoff\n\nA Noodle MCP App for **Acme Bistro**, a fictional restaurant. It is the flagship for the **end-to-end\ntransaction** pattern: the customer browses the menu and builds the order *in chat*, and the order\ncompletes in chat — only **payment** hands off, via a signed checkout deep link (the card never touches\nthe app). It pairs a view-backed `tool` menu/cart with app-only `tool` cart helpers and a model-visible\n`create_checkout` tool backed by `handoff.allowedDomains`.\n\nCapability slot: **end-to-end in-chat transaction + payment-only handoff**, plus a worked **design-first**\ndeliverable set (`design/` — a UX Document, a single-file HTML wireframe with an embedded OpenAI Apps SDK\ncompliance audit, and a Recommended API contract). It sets the quality bar the `noodle-seed` skill's\n`references/experience-design.md` and `references/app-directory-compliance.md` teach. (Distinct from\n`food-ordering`, which is the broad widget-composition proof; this one owns the design-first end-to-end +\ncompliance exemplar.)\n\n## Design deliverables (the standard to match)\n\n- [`design/UX-Document.md`](design/UX-Document.md) — the house-style UX Document.\n- [`design/wireframe.html`](design/wireframe.html) — the single-file wireframe (open in a browser) with the\n embedded OpenAI Apps SDK compliance audit.\n- [`design/api-contract.md`](design/api-contract.md) — the Recommended API shapes for the partner's kitchen/\n ordering backend.\n\n## Local author loop\n\n```sh\nnoodle validate\nnoodle test\nnoodle dev\n```\n\nIn another terminal:\n\n```sh\nnoodle tools list\nnoodle tools call show_menu --args '{\"customer\":\"Asha\"}'\nnoodle tools call create_checkout --args '{\"customer\":\"Asha\",\"cartToken\":\"cart_1\",\"total\":36}'\nnoodle check --target chatgpt\n```\n\n## Deploy\n\n```sh\nnoodle link --org demo --app acme-bistro\nnoodle deploy --access owner-only\nnoodle open\n```\n\nThis example has no connector secrets and does not include tokens, caller-key mechanisms, or\n`.env.noodle` values. The menu, prices, and URLs are fictional; payment is handled off-app on\n`acme.example`, never in chat.\n" },
|
|
17
18
|
{ relPath: "examples/acme-bistro/design/UX-Document.md", content: "# Acme Bistro ChatGPT App — User Flow & Experience Document\n\n**Prepared by:** Noodle Seed\n**Scope:** Diners browse the Acme Bistro menu, build and confirm an order inside ChatGPT, then hand off once to a signed checkout link to pay — the card never touches the app.\n**Status:** Design specification (v1)\n**Funnel boundary:** IN the app — menu, order building, order confirmation, and the checkout hand-off, all in-chat. OFF-app — **payment only**, on Acme Bistro's PCI-scoped checkout at `pay.acme.example`. **No per-user OAuth in this app**; the connector runs on Acme's own service credentials, and the diner authenticates (if at all) only on the payment page.\n\n---\n\n## 0. The One-Paragraph Thesis\n\nA hungry diner opens ChatGPT and types *\"order me two margheritas and a lemon tart from Acme Bistro.\"* Today that intent scatters across a search, a delivery-app download, a menu scroll, and a checkout form. Acme Bistro collapses it into one conversation: the model reads the live menu, parses the order out of plain language, renders a running cart the diner can nudge with a tap or a sentence, and — only when the order is right — mints a **signed, expiring payment link** that opens Acme's own checkout. We own the entire pre-payment experience; Acme owns the money. That split is deliberate and it is the product: the app never sees a card number, so Acme's PCI scope never grows, yet the diner completes a real, paid-intent order without leaving the chat. For a single restaurant, this is the cheapest possible storefront on the fastest-growing surface — one `server.ts`, no app to install, and every order arrives at Acme's checkout already built. If you can convincingly finish this order in a sentence, you have out-competed every tap-driven ordering app on the one axis they cannot copy: language.\n\n---\n\n## 1. Acme Bistro Product Overview (Knowledge Base)\n\n**Acme Bistro is a single fictional neighbourhood restaurant** offering a short, curated menu for pickup ordering. Unlike a marketplace aggregator, there is one kitchen, one menu, and one checkout — which makes the conversational surface tight and the guardrails simple. The ChatGPT App is Acme's storefront on ChatGPT: it shows the menu, builds the order, and passes a ready cart to Acme's payment page.\n\n### 1.1 The Menu (authoritative — the app must know this exactly)\n\n| Item | ID | Price (USD) | Course |\n|------|-----|-------------|--------|\n| Stone-baked Margherita | `stone_pizza` | $14 | Mains |\n| Harvest Roast Bowl | `roast_bowl` | $13 | Mains |\n| House Garden Salad | `house_salad` | $11 | Starters |\n| Lemon Tart | `lemon_tart` | $8 | Desserts |\n| Sparkling Water | `sparkling` | $4 | Drinks |\n\nPrices are whole-dollar and fixed for v1. The **backend owns pricing** — the widget sums line items for display, but the amount that reaches checkout is recomputed and re-validated by Acme at `pay.acme.example`. The menu is small enough to render in a single inline widget with no pagination.\n\n### 1.2 The End-to-End Model (the defining choice)\n\nEvery other decision follows from one line: **the order is built and confirmed in chat; only payment hands off.** There is no in-chat card capture, no wallet, no stored payment method. When the diner is ready, the app calls `create_checkout`, which returns a **signed deep link** carrying a url-safe cart token and the numeric total; ChatGPT opens it, and Acme's checkout takes the card. The MCP server is never in the payment path.\n\n### 1.3 Business Model & Why Acme Wants This\n\nAcme's bottleneck is reach, not kitchen capacity: a neighbourhood restaurant has no realistic way onto a conversational surface without building an app. The ChatGPT App removes that bottleneck for the cost of one authored server. **Attribution is built in** — every checkout link carries `src=chatgpt`, so Acme can measure exactly how much revenue the conversational storefront drives against their existing web orders.\n\n---\n\n## 2. Competitive Landscape — Food Ordering on ChatGPT\n\n| Pattern | Examples | Strength | Gap Acme fills |\n|---------|----------|----------|----------------|\n| **Marketplace aggregators** | Large delivery apps | Vast networks, delivery logistics | Menu markups, no single-restaurant intimacy, heavy handoff to a separate app |\n| **Reservation / discovery** | Booking + reviews apps | Strong discovery inventory | No ordering, no checkout |\n| **Acme Bistro (this app)** | — | One kitchen, honest single-menu pricing, full order built in chat, payment on Acme's own PCI checkout | — |\n\n**Acme's position:** Acme is not trying to be a marketplace. Its advantage inside ChatGPT is **directness** — a diner who already wants Acme's food gets from craving to a paid-ready cart in one conversation, with the restaurant's own prices and the restaurant's own checkout. The single-restaurant scope is a feature: no ranking to game, no cross-restaurant carts, no ambiguity about whose menu the model is grounding on.\n\n---\n\n## 3. Target User Personas\n\n### Persona A — \"The Regular\"\nOrders from Acme every week and knows the menu. Wants the shortest possible path: *\"the usual — two margheritas and a sparkling water.\"* Values speed and an accurate cart over discovery.\n\n### Persona B — \"The Craver\"\nArrives with an appetite, not a specific dish: *\"something light from Acme\"* or *\"what mains do you have?\"* Needs the menu surfaced fast and an opinionated nudge toward the roast bowl or the salad.\n\n### Persona C — \"The Careful Orderer\"\nHas a dietary constraint and asks before adding: *\"is the garden salad vegetarian?\"* Needs honest, non-guessing answers grounded only in what the menu data actually states — and a clear defer-to-restaurant when it doesn't.\n\n### Persona D — \"The Group Coordinator\"\nOrdering for two or three people with a running budget: *\"add a margherita, a roast bowl, a salad, and a lemon tart — what's the total?\"* Needs a live, legible cart total and easy quantity edits before committing to pay.\n\n---\n\n## 4. Conversational User Flow\n\n### 4.1 Entry Points\n\nNatural phrases that should trigger the app:\n\n```\n\"Show me the Acme Bistro menu\"\n\"Order two margheritas and a lemon tart from Acme\"\n\"I want something light from Acme Bistro\"\n\"What mains does Acme have?\"\n\"Add a sparkling water to my Acme order\"\n\"What's my Acme total?\"\n\"Check out and pay for my Acme order\"\n```\n\n### 4.2 Flow Architecture\n\n```\n┌──────────────────────────────────────────────┐\n│ USER ENTERS CHAT │\n│ (natural-language prompt) │\n└───────────────────────┬────────────────────────┘\n │\n ▼\n ┌─────────────────────────────┐\n │ show_menu (widget) │\n │ MenuCart renders: 5 items, │\n │ steppers, live total, CTA │\n └──────────────┬───────────────┘\n │\n ┌───────────────┼────────────────┐\n ▼ ▼ ▼\n┌──────────────┐ ┌──────────────┐ ┌──────────────┐\n│ add_to_cart │ │remove_from_ │ │ taps in │\n│ (NL: \"two │ │cart (NL or │ │ the widget │\n│ margheritas\")│ │ − button) │ │ (+ / −) │\n└──────┬───────┘ └──────┬───────┘ └──────┬───────┘\n └────────────────┼────────────────┘\n ▼\n ┌─────────────────────────────┐\n │ Live cart + running total │\n │ (summed in the widget) │\n └──────────────┬───────────────┘\n │ \"check out & pay\"\n ▼\n ┌─────────────────────────────┐\n │ create_checkout │\n │ → signed, expiring link to │\n │ pay.acme.example │\n └──────────────┬───────────────┘\n ▼ (OFF-APP)\n ┌─────────────────────────────┐\n │ Acme PCI checkout · card │\n │ entered here, never in chat │\n └─────────────────────────────┘\n```\n\n### 4.3 Detailed Conversational Scenarios\n\n#### Scenario 1 — Natural-language order (Persona A/D)\n\n```\nUSER: Order me two margheritas and a lemon tart from Acme Bistro.\n\nAPP: [tool: show_menu {customer:\"Guest\"}] → renders MenuCart\n [tool: add_to_cart {item:\"stone_pizza\", quantity:2}]\n [tool: add_to_cart {item:\"lemon_tart\", quantity:1}]\n\n Here's your Acme Bistro order — two Stone-baked Margheritas\n and a Lemon Tart. That's $36 so far. Want a drink or a side,\n or shall I take you to checkout to pay?\n\n [MenuCart widget]\n ┌────────────────────────────────────────────┐\n │ 🍽 Acme Bistro 2 in cart │\n │ Stone-baked Margherita Mains $14 [−]2[+]│\n │ Harvest Roast Bowl Mains $13 [−]0[+]│\n │ House Garden Salad Starters$11 [−]0[+]│\n │ Lemon Tart Desserts $8 [−]1[+]│\n │ Sparkling Water Drinks $4 [−]0[+]│\n │ ────────────────────────────────────────── │\n │ Total $36 [ 💳 Check out & pay ] │\n │ Payment happens on acme.example — your card │\n │ is never entered in chat. │\n └────────────────────────────────────────────┘\n```\n\n#### Scenario 2 — Add and edit (Persona D)\n\n```\nUSER: Add a sparkling water too.\n\nAPP: [tool: add_to_cart {item:\"sparkling\", quantity:1}]\n Added a Sparkling Water — your total is $40 for four items.\n\nUSER: Actually drop one margherita.\n\nAPP: [tool: remove_from_cart {item:\"stone_pizza\"}]\n Done — one Margherita, one Lemon Tart, one Sparkling Water.\n New total $26. Ready to pay whenever you are.\n```\n\n#### Scenario 3 — Dietary question, honest answer (Persona C)\n\n```\nUSER: Is the garden salad vegetarian?\n\nAPP: The House Garden Salad is on our Starters menu at $11. I can\n tell you what's on the menu, but I can't confirm ingredient-\n level details like preparation or cross-contact — for\n allergy or dietary certainty, please check with Acme Bistro\n directly before ordering. Want me to add it anyway, or show\n you the other starters and mains?\n```\n\n#### Scenario 4 — Checkout handoff (all personas)\n\n```\nUSER: Check out and pay.\n\nAPP: [tool: create_checkout {customer:\"Guest\",\n cartToken:\"stone_pizzax2-lemon_tartx1-sparklingx1\", total:40}]\n\n Your order's ready — two Margheritas, a Lemon Tart, and a\n Sparkling Water, $40 total. I've opened Acme's secure\n checkout to take payment. Your card is entered on\n acme.example, never here in chat.\n\n [opens https://pay.acme.example/checkout?cart=\n stone_pizzax2-lemon_tartx1-sparklingx1&total=40&src=chatgpt]\n```\n\n---\n\n## 5. UI Widget Specifications\n\n> The widget is authored as a Noodle Seed React `view` (`tool` → `MenuCart`), styled with **vanilla CSS cascade layers** so it inherits the host's light/dark theme and adapts to ChatGPT's surface. Compliance is verified with `noodle check --target chatgpt`.\n\n### 5.1 Design System Compliance\n\nAcme authors **one** brand surface through the server `branding` tokens; everything else defers to host-provided semantic tokens (text, background, border, success/warning), so the widget looks native in ChatGPT.\n\n| Category | Source | Value |\n|----------|--------|-------|\n| Text / background / border | Host semantic tokens (via cascade layers) | Host-provided, theme-aware |\n| Brand accent | `branding.accent` | `#B91C1C` (Acme red) — **primary CTA + logo mark only** |\n| Surface (light) | `branding.surface` | `#FEF3F2` |\n| Surface (dark) | `branding.surfaceDark` | `#1A1211` |\n| Radius / density | `branding.radius` / `branding.density` | `lg` / `comfortable` |\n\n**Enforced rules:** system font stack; monochromatic outlined icons (the plate mark, the card glyph); WCAG AA contrast on all text/surface pairs (Acme red is used only as a fill behind light text or as a 1px mark, never as body text on white); no nested scroll (the 5-item menu fits without an inner scroller); brand accent restricted to the primary **Check out & pay** button and the header mark.\n\n### 5.2 Display Mode Strategy\n\n| User intent | Display mode | Rationale |\n|-------------|--------------|-----------|\n| Browse the menu / build an order | **Inline Card** (`MenuCart`) | Five items + steppers + total fit an inline card; no drill-in, no pagination |\n| Confirm total & pay | **Inline Card** (same widget, primary CTA) | The CTA opens the off-app checkout; no in-chat payment surface |\n| Payment | **None (off-app browser)** | Deliberately not a widget — card capture stays on Acme's PCI page |\n\nModes deliberately **not** used: no Carousel (a single flat menu doesn't need one), no Fullscreen (five items don't warrant it), no Picture-in-Picture (there is no live-tracking phase in v1 — fulfilment happens after payment on Acme's side).\n\n### 5.3 Widget Specifications\n\n#### ★ `MenuCart` — Inline Card\n**Purpose:** the entire in-chat experience — menu, order building, live total, and the checkout hand-off — in one widget.\n\n| Spec | Value |\n|------|-------|\n| Header | Plate mark, \"Acme Bistro\" title, status subtitle, cart chip (`N in cart` / `Fullscreen`) |\n| Menu rows | One per item: name, course, price, and a `[− qty +]` stepper |\n| Total | Live subtotal summed in React from the session-local cart |\n| Primary action | **Check out & pay** (brand red) — disabled while the cart is empty or checkout is pending; opens the signed link via `openExternal` |\n| Reassurance | Fine-print note: \"Payment happens on acme.example — your card is never entered in chat.\" |\n| Edge states | Empty cart (CTA disabled), pending checkout (\"Opening checkout…\"), dark theme variant |\n\n**Two-users note:** every row is model-fillable — the model reflects *\"two margheritas\"* into `add_to_cart {item:\"stone_pizza\", quantity:2}`, and the same widget a human taps updates identically.\n\n---\n\n## 6. Tool Definitions (App Backend)\n\n### ★ Tool 1: `show_menu` — `tool`\n**Input:** `{ customer?: string = \"Guest\" }`\n**Output:** `{ status, customer, items[] }` where each item is `{ id, name, price, kind }`.\n**Renders:** the `MenuCart` widget.\n**Annotations:** read-only.\n**Triggers:** any menu / ordering intent (\"show me Acme's menu\", \"order from Acme\").\n\n### Tool 2: `add_to_cart` — `tool`\n**Input:** `{ customer?, item: <menu id> = \"stone_pizza\", quantity?: int ≥1 = 1, notes?: string }`\n**Output:** `{ status, item, quantity, notes }`.\n**Annotations:** local write (non-destructive).\n**Triggers:** natural-language additions (\"add two margheritas\", \"and a lemon tart\"). Widget-facing helper — reflects NL selections into the visible cart.\n\n### Tool 3: `remove_from_cart` — `tool`\n**Input:** `{ customer?, item: <menu id> = \"stone_pizza\" }`\n**Output:** `{ status, item }`.\n**Annotations:** local write (non-destructive).\n**Triggers:** \"drop one margherita\", \"remove the salad\", or the widget's `−` button.\n\n### ★ Tool 4: `create_checkout` — model-visible `tool`\n**Input:** `{ customer?, cartToken: string = \"cart\", total: number ≥0 = 0 }`\n**Output:** `{ status, summary, checkoutUrl }`.\n**Annotations:** open-link (external action).\n**Behaviour:** returns a signed deep link — `https://pay.acme.example/checkout?cart=<cartToken>&total=<total>&src=chatgpt`. The card never reaches this app. `handoff.allowedDomains` includes `pay.acme.example`, so the compiler derives the ChatGPT redirect domain and the link opens without a safe-link warning.\n**Triggers:** \"check out\", \"pay\", \"I'm done\".\n\nTools are atomic and model-friendly: `show_menu` reads, the two cart tools write locally, `create_checkout` opens the one external link. There is no `submit_order` or `capture_payment` tool by design — order fulfilment and payment are Acme's, past the boundary.\n\n---\n\n## 7. Conversation Design Principles\n\n### 7.1 Tone of Voice\nWarm, concise, and restaurant-first — like a counter host who knows the menu. State prices and totals as plain facts (\"that's $36 so far\"), recommend when it helps (\"the roast bowl is the heartier main\"), and never oversell.\n\n### 7.2 Guardrails (non-negotiable)\n- **Never invent menu items or prices.** Ground every dish and amount in the `show_menu` data — only the five items, only their listed prices.\n- **Never confirm allergen or dietary safety.** State what the menu says (course, name, price); for ingredient-level or cross-contact questions, defer to Acme Bistro directly. Never assert \"this is vegetarian/gluten-free\" without a menu flag that says so.\n- **Never take payment in chat.** No card numbers, no CVV, no wallet. Payment is the one off-app step; if a user pastes card details, decline and point them to the checkout link.\n- **Never promise fulfilment the app can't see.** The app builds and hands off the order; pickup timing and order status live on Acme's side after payment.\n- **Always show the honest total before checkout**, and restate that payment happens on `acme.example`.\n\n### 7.3 Memory Strategy\nRemember the diner's in-session cart and name. There is no cross-session account (no per-user auth) — a returning diner starts a fresh order, though the model may recall a prior order *within the same conversation* to speed a reorder.\n\n### 7.4 Multi-Turn Intelligence\nThe model infers item + quantity from language (\"a couple of margheritas\" → `quantity:2`), keeps a running total in view, and asks only when genuinely ambiguous (\"did you mean the margherita or the roast bowl?\"). It never asks for a field it can default.\n\n---\n\n## 8. End-to-End User Journey Map\n\n**Phase 1 — Menu (first 5–10s):** user names Acme or asks for the menu → `show_menu` renders `MenuCart`.\n**Phase 2 — Build (10–40s):** natural-language adds/removes (`add_to_cart` / `remove_from_cart`) and/or widget steppers; the total updates live.\n**Phase 3 — Confirm (5–10s):** the app restates the cart and total in plain language; user says \"pay\".\n**Phase 4 — Hand off (2–5s):** `create_checkout` mints the signed link; ChatGPT opens it.\n**Phase 5 — Pay (off-app):** the diner enters their card on `pay.acme.example`; the app's job is done. Fulfilment is Acme's.\n\n---\n\n## 9. Handoff Architecture (Deep Dive)\n\n**What must be true of the handoff:**\n1. **Context survives the jump.** The cart token encodes every line (`stone_pizzax2-lemon_tartx1-sparklingx1`) plus the total, so Acme's checkout rehydrates the exact order without a second round-trip.\n2. **The link is signed and attributable.** Acme signs the checkout URL server-side and carries `src=chatgpt` for attribution. `handoff.allowedDomains: ['https://pay.acme.example', 'https://acme.example']` lets the compiler emit the redirect domain so the link opens cleanly.\n3. **State is re-validated past the boundary.** Acme recomputes pricing, checks inventory, and enforces the total at checkout — the widget's sum is display-only and never authoritative.\n4. **The link expires.** Checkout URLs carry an `expires_at`; a stale link lands on a \"cart expired — start again\" page rather than charging an out-of-date total.\n\n**URL pattern:** `https://pay.acme.example/checkout?cart={cartToken}&total={total}&src=chatgpt`\n\n**Why payment is the only handoff:** keeping card capture on Acme's PCI-scoped checkout means the MCP server never enters payment scope — no card data, no stored methods, no compliance burden added by the ChatGPT surface. The app is the storefront; Acme is the register.\n\n**Open questions for Acme engineering:**\n- Signing scheme + default expiry window (proposed: 15 minutes)?\n- Should the cart token be opaque (server-minted) instead of the human-readable `idxN-idxN` form, to prevent client-side total tampering before re-validation?\n- Post-payment visibility (webhook / polling) so a later chat turn can confirm \"your order is ready\" — out of scope for v1?\n\n---\n\n## 10. Demo Scope Recommendation\n\n**MVP (build in this order):**\n1. `show_menu` + `MenuCart` — menu renders, steppers work, total sums live.\n2. `add_to_cart` / `remove_from_cart` — natural-language and button edits both reflect in the cart.\n3. `create_checkout` — signed link opens Acme's checkout with the cart pre-loaded.\n\n**2-minute demo script:**\n```\nNARRATOR: \"A diner wants dinner from their neighbourhood spot,\nAcme Bistro, without leaving ChatGPT.\"\n\nUSER: \"Order two margheritas and a lemon tart from Acme Bistro.\"\n[show_menu renders MenuCart; add_to_cart ×2 fills the cart — $36]\n\nUSER: \"Add a sparkling water.\"\n[add_to_cart — total ticks to $40]\n\nUSER: \"What's my total?\"\n[MenuCart shows Total $40, four items]\n\nUSER: \"Check out and pay.\"\n[create_checkout mints the signed link; ChatGPT opens\n pay.acme.example — card entered there, never in chat]\n\nNARRATOR: \"Built and confirmed in one conversation; paid on Acme's\nown secure checkout. The app never saw a card number.\"\n```\n\n---\n\n## 11. Technical Architecture (High Level)\n\n```\n┌───────────────────────────────────────────────┐\n│ ChatGPT Client │\n│ MenuCart widget (Noodle Seed React view) │\n│ cascade-layer CSS · host theme tokens │\n└───────────────────────┬─────────────────────────┘\n │ tool calls\n ▼\n┌───────────────────────────────────────────────┐\n│ Acme Bistro MCP server (Noodle Seed) │\n│ show_menu · add_to_cart · remove_from_cart │\n│ create_checkout │\n│ branding tokens · handoff.allowedDomains │\n│ (static menu data; no per-user auth) │\n└───────────────────────┬─────────────────────────┘\n │ signed checkout link (no card data)\n ▼\n┌───────────────────────────────────────────────┐\n│ Acme Bistro checkout — pay.acme.example │\n│ PCI-scoped card capture · pricing/inventory │\n│ validation · order fulfilment │\n└───────────────────────────────────────────────┘\n```\n\nMenu data is static in v1 (authored in `server.ts`). Session cart state lives in the widget (React). No database, no per-user credentials, no card data in the MCP server — the smallest possible surface for a single-restaurant storefront.\n\n---\n\n## 12. Success Metrics\n\n| Metric | Target | Measurement |\n|--------|--------|-------------|\n| Menu render → first add | 55%+ of `show_menu` sessions add ≥1 item | `add_to_cart` call rate |\n| Cart with ≥1 item → `create_checkout` | 60%+ | Tool-call funnel |\n| Checkout link opened → paid (on Acme) | Acme-side; joined via `src=chatgpt` | Acme checkout analytics |\n| End-to-end (entry → paid order) | 15%+ | Funnel + Acme attribution |\n| Average order value | $30+ | Cart totals at `create_checkout` |\n| Time to checkout hand-off | Under 60s | Session duration |\n\n**Attribution mechanics:** every `create_checkout` link carries `src=chatgpt`, so Acme can attribute paid revenue to the ChatGPT storefront and compare it against their existing web channel.\n\n---\n\n## 13. Future Enhancements (Post-Launch)\n\n- **Item notes at scale** — surface the `notes` field in the widget for per-item requests (\"no basil\").\n- **Modifier groups** — sizes / add-ons if the menu grows beyond flat items (would introduce an item-detail widget).\n- **Live inventory** — mark sold-out items unavailable from Acme's kitchen system.\n- **Post-payment confirmation** — an Acme webhook so a later chat turn can confirm \"your order is ready for pickup.\"\n- **Returning-diner reorder** — opt-in, if Acme later adds per-user accounts (would move this app off the no-auth model deliberately).\n- **Scheduled pickup** — choose a pickup window before the checkout hand-off.\n- **Second location** — a lightweight location picker if Acme opens another kitchen (keeps single-menu simplicity per location).\n\n---\n\n### Appendix A — Funnel Boundary Cheat-Sheet\n\n| User request | Handled in the app? | Where it lands |\n|--------------|---------------------|----------------|\n| \"Show me the menu\" | ✅ In-chat | `show_menu` → `MenuCart` |\n| \"Add two margheritas\" | ✅ In-chat | `add_to_cart` |\n| \"Drop the salad\" | ✅ In-chat | `remove_from_cart` |\n| \"What's my total?\" | ✅ In-chat | Live widget total |\n| \"Check out / pay\" | ✅ In-chat → hand-off | `create_checkout` mints signed link |\n| Enter card & pay | ❌ OFF-APP | `pay.acme.example` (Acme PCI checkout) |\n| Pickup timing / order status | ❌ OFF-APP | Acme's side, post-payment |\n| Account / saved cards | ❌ Not in v1 | No per-user auth by design |\n\n---\n\n*This document is the master spec for the Acme Bistro ChatGPT App. Acme Bistro, its menu, and its domains are illustrative. All tool names, widget names, menu items, and prices match the runnable Noodle Seed app exactly; the checkout link shape and pricing/inventory validation are owned by Acme's backend past the funnel boundary.*\n" },
|
|
18
19
|
{ relPath: "examples/acme-bistro/design/api-contract.md", content: "# Acme Bistro — Recommended API Shapes\n\n**For Acme Bistro Engineering.** Concrete request/response JSON for each tool the ChatGPT App calls, so your backend can implement exactly what the app needs. These are a starting point for the contract, not a final spec — field names and envelopes can shift to match Acme's platform conventions, as long as the semantics below are preserved.\n\n> **The backend owns pricing, inventory, and payment.** The widget sums line items for *display only*; the amount that reaches checkout is recomputed and enforced by Acme at `pay.acme.example`. The MCP server never sees a card number, a CVV, or a stored payment method — payment is the single off-app step. Keep pricing, availability, and the signed checkout link server-side.\n\nThe app maps to four tools:\n\n| Tool | Kind | Job |\n|------|------|-----|\n| `show_menu` | `tool` (read-only) | Return the menu + render the `MenuCart` widget |\n| `add_to_cart` | `tool` (local write) | Reflect a natural-language addition into the visible cart |\n| `remove_from_cart` | `tool` (local write) | Remove one unit of an item |\n| `create_checkout` | model-visible `tool` (open-link) | Mint the signed, expiring payment link |\n\nMenu item IDs are the stable enum: `stone_pizza`, `roast_bowl`, `house_salad`, `lemon_tart`, `sparkling`.\n\n---\n\n## 1. `show_menu` — menu + widget\n\nThe one read. Returns the full menu (small enough to render without pagination) plus a status line the model can speak.\n\n**Request**\n```json\n{\n \"customer\": \"Guest\"\n}\n```\n\n**Response**\n```json\n{\n \"status\": \"Acme Bistro menu is ready for Guest. Build the order here; pay at checkout.\",\n \"customer\": \"Guest\",\n \"items\": [\n { \"id\": \"stone_pizza\", \"name\": \"Stone-baked Margherita\", \"price\": 14, \"kind\": \"Mains\" },\n { \"id\": \"roast_bowl\", \"name\": \"Harvest Roast Bowl\", \"price\": 13, \"kind\": \"Mains\" },\n { \"id\": \"house_salad\", \"name\": \"House Garden Salad\", \"price\": 11, \"kind\": \"Starters\" },\n { \"id\": \"lemon_tart\", \"name\": \"Lemon Tart\", \"price\": 8, \"kind\": \"Desserts\" },\n { \"id\": \"sparkling\", \"name\": \"Sparkling Water\", \"price\": 4, \"kind\": \"Drinks\" }\n ]\n}\n```\n\n**Notes.**\n- `price` is a whole-dollar USD number in v1. If Acme moves to cents or a currency field, keep one canonical numeric price per item so the widget's sum and the checkout total agree.\n- `items[]` is the authoritative menu — the model must not invent dishes or prices outside this list.\n- **Extensibility:** Acme may add fields (`description`, `available`, `dietary_tags`, `image_url`) without breaking the app, as long as `id`, `name`, `price`, and `kind` remain. If `available: false` is added, the widget should disable that row's `+` button.\n\n---\n\n## 2. `add_to_cart` — reflect a natural-language addition\n\nCalled when the diner says *\"add two margheritas\"* — the model fills `item` and `quantity` from language. The cart is session-local in the widget; this tool echoes the resolved selection so the model can speak it back.\n\n**Request**\n```json\n{\n \"customer\": \"Guest\",\n \"item\": \"stone_pizza\",\n \"quantity\": 2,\n \"notes\": \"\"\n}\n```\n\n**Response**\n```json\n{\n \"status\": \"Added 2 × stone_pizza for Guest.\",\n \"item\": \"stone_pizza\",\n \"quantity\": 2,\n \"notes\": \"\"\n}\n```\n\n**Notes.**\n- `quantity` is an integer ≥ 1 (defaults to 1). `item` must be one of the five menu IDs; reject unknown IDs.\n- `notes` is a free-text per-item request (\"no basil\"); optional, defaults to empty.\n- **If Acme makes this server-authoritative** (rather than widget-local), return the updated line and a running subtotal so the frontend can render without a second call — e.g. add `line_total` and `cart_subtotal`. For v1 the widget owns the running total, so the minimal echo above is sufficient.\n\n---\n\n## 3. `remove_from_cart` — remove one unit\n\nCalled by the widget's `−` button or by language (\"drop a margherita\"). Removes one unit of the item.\n\n**Request**\n```json\n{\n \"customer\": \"Guest\",\n \"item\": \"stone_pizza\"\n}\n```\n\n**Response**\n```json\n{\n \"status\": \"Removed stone_pizza for Guest.\",\n \"item\": \"stone_pizza\"\n}\n```\n\n**Notes.**\n- Removing decrements by one; the widget deletes the line when its quantity reaches zero.\n- No error if the item isn't in the cart — the operation is idempotent from the model's view (the widget guards the `−` button when quantity is 0).\n\n---\n\n## 4. `create_checkout` — mint the signed payment link\n\nThe one handoff. The widget computes the total (live React) and passes a url-safe cart token plus the numeric total; the tool returns a **signed, expiring** deep link to Acme's PCI-scoped checkout. **No card data is exchanged here** — the diner enters their card on `pay.acme.example`.\n\n**Request**\n```json\n{\n \"customer\": \"Guest\",\n \"cartToken\": \"stone_pizzax2-lemon_tartx1-sparklingx1\",\n \"total\": 40\n}\n```\n\n**Response**\n```json\n{\n \"status\": \"Ready to pay for Guest's order.\",\n \"summary\": \"Guest's Acme Bistro order · 40 USD\",\n \"checkoutUrl\": \"https://pay.acme.example/checkout?cart=stone_pizzax2-lemon_tartx1-sparklingx1&total=40&src=chatgpt\",\n \"expires_at\": \"2026-07-09T18:15:00-07:00\"\n}\n```\n\n**Notes.**\n- **`checkoutUrl` must be signed server-side.** The `cart` and `total` query params are a convenience for rehydration and display — Acme's checkout must **recompute pricing from the cart token and enforce its own total**, never trusting the client-supplied `total`. Treat the incoming `total` as a display hint to reconcile, not as the charge amount.\n- **`expires_at`** bounds the link (proposed 15-minute window). An expired link should land on a \"cart expired — start again\" page, not charge a stale total. The runnable app returns `status`, `summary`, and `checkoutUrl`; adding `expires_at` is the recommended production extension so the model can tell the diner how long the link is good for.\n- **`src=chatgpt`** is the attribution parameter — carry it through to Acme's order record so ChatGPT-sourced revenue is measurable against the web channel.\n- **Cart token format.** The app emits a human-readable `<id>x<qty>-<id>x<qty>` token. For production, consider an **opaque server-minted token** (the client passes a cart handle; Acme resolves it to the authoritative lines) to remove any incentive to tamper with the token or `total` before re-validation.\n- **`handoff.allowedDomains`** in the server (`https://pay.acme.example`, `https://acme.example`) is what lets the compiler derive the ChatGPT redirect domain, so the link opens without a safe-link warning. Any new payment domain must be added there.\n\n---\n\n## Validation & ownership summary\n\nThe backend must own and enforce:\n\n1. **Pricing** — the authoritative per-item price and the order total; the widget sum is display-only.\n2. **Inventory** — item availability at menu-read and at checkout; a sold-out item should not reach a paid order.\n3. **Payment** — all card capture on `pay.acme.example`, inside Acme's PCI scope. The MCP server is never in the payment path.\n4. **Link integrity** — server-side signing of `checkoutUrl`, an enforced `expires_at`, and recomputation of the total from the cart token before charging.\n\nEverything before payment — menu, cart, total, and minting the link — is the ChatGPT App's job. Everything at and after payment is Acme's.\n\n---\n\n## Open questions for Acme engineering\n\n1. **Cart token shape** — keep the readable `idxN-idxN` form, or move to an opaque server handle to prevent client-side tampering?\n2. **Signing scheme & expiry** — HMAC, JWT, or signed query params, and what default expiry window (proposed 15 min)?\n3. **Currency & precision** — stay whole-dollar USD, or introduce cents / a `currency` field? The widget and checkout total must agree.\n4. **Server-authoritative cart** — should `add_to_cart` / `remove_from_cart` become backend-owned (returning subtotals), or stay widget-local for v1?\n5. **Post-payment visibility** — expose a webhook or polling endpoint so a later chat turn can confirm order status? Out of scope for v1.\n" },
|
|
19
20
|
{ relPath: "examples/acme-bistro/design/wireframe.html", content: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Acme Bistro × ChatGPT — End-to-End Wireframes</title>\n<style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap');\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: 'Inter', -apple-system, sans-serif; background: #f4f5f7; color: #1a1211; line-height: 1.55; }\n\n /* ── Acme Bistro brand tokens (from server branding) ── */\n :root {\n --acme-red: #B91C1C;\n --acme-red-deep: #991B1B;\n --acme-red-soft: #FEF3F2;\n --acme-red-border: #FBD5D2;\n --acme-dark: #1A1211;\n --green: #15803D; --green-soft: #F0FDF4; --green-border: #BBF7D0;\n --amber: #B45309; --amber-soft: #FFFBEB; --amber-border: #FDE68A;\n }\n\n /* ── Page Header ── */\n .page-header { background: #fff; border-bottom: 1px solid #e0dddb; padding: 30px 48px; position: sticky; top: 0; z-index: 100; }\n .page-header h1 { font-size: 22px; font-weight: 800; letter-spacing: -0.4px; }\n .page-header h1 .brand { color: var(--acme-red); }\n .page-header p { font-size: 13px; color: #8a8580; margin-top: 4px; }\n .page-header .scope { display: inline-block; margin-top: 10px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; padding: 5px 13px; background: var(--acme-dark); color: #fff; border-radius: 5px; }\n .page-header .scope b { color: var(--acme-red-border); }\n\n /* ── Section Nav ── */\n .section-nav { background: #fff; border-bottom: 1px solid #eee; padding: 11px 48px; display: flex; gap: 22px; font-size: 12px; font-weight: 600; position: sticky; top: 92px; z-index: 99; }\n .section-nav a { color: #8a8580; text-decoration: none; }\n .section-nav a:hover { color: var(--acme-red); }\n\n .container { max-width: 1440px; margin: 0 auto; padding: 36px 48px 80px; }\n\n /* ── Legend ── */\n .vocab { display: flex; gap: 16px; flex-wrap: wrap; margin: 0 0 20px; padding: 14px 18px; background: #fff; border: 1px solid #e6e3e0; border-radius: 12px; font-size: 12px; color: #555; }\n .vocab-item { display: flex; align-items: center; gap: 8px; }\n .vocab-sw { width: 16px; height: 16px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.08); }\n .sw-red { background: var(--acme-red); } .sw-green { background: var(--green); }\n .sw-amber { background: var(--amber); } .sw-dark { background: var(--acme-dark); }\n .sw-grey { background: #cfcac5; } .sw-dash { background: repeating-linear-gradient(45deg,#fff,#fff 3px,#cfcac5 3px,#cfcac5 5px); }\n\n /* ── Section ── */\n .section { margin-bottom: 56px; }\n .section-label { font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: #a39d97; margin-bottom: 8px; display: block; }\n .section-title { font-size: 27px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 6px; }\n .section-subtitle { font-size: 14px; color: #6b655f; margin-bottom: 22px; max-width: 820px; }\n\n /* ── Rationale Block ── */\n .rationale { background: #fff; border: 1px solid #e6e3e0; border-left: 3px solid var(--acme-red); border-radius: 10px; padding: 16px 20px; margin-bottom: 22px; max-width: 960px; }\n .rationale h4 { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #a39d97; margin-bottom: 9px; }\n .rationale p { font-size: 13px; color: #46413c; line-height: 1.65; margin-bottom: 8px; }\n .rationale p:last-child { margin-bottom: 0; }\n .r-tag { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; margin-right: 4px; letter-spacing: 0.3px; }\n .r-tag.ux { background: #EEF2FF; color: #4338CA; }\n .r-tag.ui { background: #ECFEFF; color: #0E7490; }\n .r-tag.acme { background: var(--acme-red-soft); color: var(--acme-red-deep); }\n .r-tag.trust { background: var(--green-soft); color: var(--green); }\n code { background: #f1efec; padding: 1px 5px; border-radius: 3px; font-size: 11px; font-family: 'JetBrains Mono', monospace; }\n\n /* ── Phone Row ── */\n .phones-row { display: flex; gap: 24px; overflow-x: auto; padding-bottom: 12px; align-items: flex-start; }\n .phone-step { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; }\n .step-label { font-size: 11px; font-weight: 700; color: #8a8580; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 12px; text-align: center; max-width: 300px; }\n .step-label small { display: block; font-weight: 400; letter-spacing: 0; text-transform: none; color: #b0aaa4; margin-top: 3px; }\n .phone { width: 300px; min-height: 600px; background: #fff; border: 2px solid var(--acme-dark); border-radius: 30px; overflow: hidden; display: flex; flex-direction: column; }\n .phone.offapp { border: 2px dashed #b0aaa4; }\n .phone-notch { width: 90px; height: 22px; background: var(--acme-dark); border-radius: 0 0 12px 12px; margin: 0 auto; flex-shrink: 0; }\n .phone.offapp .phone-notch { background: #b0aaa4; }\n .phone-screen { padding: 14px; display: flex; flex-direction: column; gap: 10px; flex: 1; }\n .step-arrow { display: flex; align-items: center; justify-content: center; flex-shrink: 0; align-self: center; width: 32px; font-size: 22px; color: #cfcac5; padding-top: 260px; }\n\n /* ── Chrome ── */\n .chatgpt-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 0 9px; border-bottom: 1px solid #eee; }\n .chatgpt-header .model-name { font-size: 13px; font-weight: 600; }\n .chatgpt-header .dots { font-size: 17px; color: #b0aaa4; letter-spacing: 2px; }\n .browser-header { display: flex; align-items: center; gap: 7px; padding: 7px 9px; background: #f1efec; border-radius: 8px; font-size: 10px; color: #6b655f; }\n .browser-header .lock { font-size: 10px; }\n .browser-header .url { font-family: 'JetBrains Mono', monospace; font-size: 9.5px; color: #46413c; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n\n /* ── Messages ── */\n .msg { max-width: 92%; font-size: 12.5px; line-height: 1.55; }\n .msg.user { align-self: flex-end; background: var(--acme-dark); color: #fff; padding: 9px 13px; border-radius: 16px 16px 4px 16px; margin-left: auto; }\n .msg.assistant { color: #1a1211; padding: 2px 0; }\n .msg.assistant strong { font-weight: 600; }\n\n /* ── Tool Call ── */\n .tool-call { display: flex; align-items: center; gap: 8px; padding: 7px 11px; background: #faf8f6; border: 1px solid #e6e3e0; border-radius: 9px; font-size: 10.5px; color: #6b655f; }\n .tool-call .icon { width: 19px; height: 19px; background: var(--acme-red); border-radius: 5px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .tool-call .icon svg { width: 12px; height: 12px; stroke: #fff; fill: none; stroke-width: 2; }\n .tool-call .label { font-weight: 700; color: var(--acme-dark); font-family: 'JetBrains Mono', monospace; font-size: 10px; }\n .tool-call .mono-tool { font-family: 'JetBrains Mono', monospace; font-size: 9.5px; color: #8a8580; }\n\n /* ── MenuCart widget ── */\n .wcard { border: 1.5px solid var(--acme-red-border); border-radius: 14px; overflow: hidden; background: #fff; }\n .wcard-head { display: flex; align-items: center; gap: 9px; padding: 11px 13px; background: var(--acme-red-soft); border-bottom: 1px solid var(--acme-red-border); }\n .wcard-head .logo { width: 26px; height: 26px; border-radius: 7px; background: var(--acme-red); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .wcard-head .logo svg { width: 15px; height: 15px; stroke: #fff; fill: none; stroke-width: 1.7; }\n .wc-title { font-size: 13px; font-weight: 800; }\n .wc-status { font-size: 10px; color: #8a8580; margin-top: 1px; line-height: 1.35; }\n .wc-chip { margin-left: auto; font-size: 9.5px; font-weight: 700; padding: 3px 8px; background: #fff; border: 1px solid var(--acme-red-border); color: var(--acme-red-deep); border-radius: 20px; white-space: nowrap; }\n .menu-row { display: flex; align-items: center; gap: 8px; padding: 9px 13px; border-bottom: 1px solid #f4f2ef; }\n .mr-main { flex: 1; }\n .mr-name { font-size: 12px; font-weight: 600; }\n .mr-kind { font-size: 9.5px; color: #a39d97; text-transform: uppercase; letter-spacing: 0.4px; }\n .mr-price { font-size: 12px; font-weight: 700; color: #46413c; }\n .mr-qty { display: flex; align-items: center; gap: 7px; }\n .step-btn { width: 20px; height: 20px; border: 1px solid #d8d3ce; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #46413c; background: #fff; }\n .step-btn.on { border-color: var(--acme-red); color: var(--acme-red); }\n .step-btn.dis { opacity: 0.3; }\n .mr-count { font-size: 12px; font-weight: 700; min-width: 12px; text-align: center; }\n .wc-foot { display: flex; align-items: center; gap: 10px; padding: 12px 13px 8px; }\n .wc-total { font-size: 13px; color: #46413c; }\n .wc-total strong { font-size: 16px; font-weight: 800; color: var(--acme-dark); margin-left: 4px; }\n .wc-cta { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; padding: 9px 15px; background: var(--acme-red); color: #fff; border: none; border-radius: 10px; font-size: 12px; font-weight: 700; cursor: pointer; }\n .wc-cta svg { width: 14px; height: 14px; stroke: #fff; fill: none; stroke-width: 2; }\n .wc-cta.dis { opacity: 0.4; }\n .wc-note { padding: 0 13px 12px; font-size: 9.5px; color: #a39d97; line-height: 1.45; }\n .wc-note b { color: var(--green); }\n\n /* ── Handoff card ── */\n .handoff-card { border: 1.5px solid var(--acme-red-border); border-radius: 14px; background: #fff; text-align: center; padding: 16px 15px 13px; }\n .handoff-card .glyph { width: 42px; height: 42px; margin: 0 auto 8px; border-radius: 11px; background: var(--acme-red-soft); display: flex; align-items: center; justify-content: center; }\n .handoff-card .glyph svg { width: 22px; height: 22px; stroke: var(--acme-red); fill: none; stroke-width: 1.6; }\n .handoff-card h4 { font-size: 13.5px; font-weight: 800; margin-bottom: 4px; }\n .handoff-card p { font-size: 11px; color: #6b655f; margin-bottom: 11px; line-height: 1.5; }\n .mini-sum { background: #faf8f6; border: 1px solid #eae7e3; border-radius: 9px; padding: 9px 11px; margin: 0 0 11px; text-align: left; }\n .mini-sum .row { display: flex; justify-content: space-between; font-size: 11px; color: #6b655f; padding: 2px 0; }\n .mini-sum .row.total { font-size: 12.5px; font-weight: 800; color: var(--acme-dark); border-top: 1px solid #eae7e3; margin-top: 4px; padding-top: 5px; }\n .pay-link { font-size: 9px; color: #a39d97; margin-top: 10px; line-height: 1.55; }\n .pay-link code { font-size: 8.5px; }\n .pay-link b { color: var(--acme-dark); }\n\n .cta-primary { display: inline-flex; align-items: center; justify-content: center; gap: 6px; width: 100%; padding: 10px; background: var(--acme-red); color: #fff; border: none; border-radius: 10px; font-size: 12.5px; font-weight: 700; cursor: pointer; margin-bottom: 6px; }\n .cta-ghost { display: block; width: 100%; padding: 9px; background: transparent; color: var(--acme-dark); border: 1.5px solid #d8d3ce; border-radius: 10px; font-size: 12px; font-weight: 600; cursor: pointer; }\n\n /* ── Off-app checkout body ── */\n .checkout-body { flex: 1; display: flex; flex-direction: column; gap: 9px; padding-top: 4px; }\n .co-brand { font-size: 14px; font-weight: 800; color: var(--acme-red); }\n .co-sub { font-size: 10px; color: #8a8580; }\n .co-field { border: 1px solid #e6e3e0; border-radius: 9px; padding: 9px 11px; }\n .co-field .lbl { font-size: 9px; color: #a39d97; text-transform: uppercase; letter-spacing: 0.5px; }\n .co-field .val { font-size: 12px; color: #46413c; margin-top: 3px; font-family: 'JetBrains Mono', monospace; }\n .co-field.card { background: var(--amber-soft); border-color: var(--amber-border); }\n .co-line { display: flex; justify-content: space-between; font-size: 11px; color: #6b655f; padding: 2px 0; }\n .co-line.total { font-weight: 800; color: var(--acme-dark); font-size: 13px; border-top: 1px solid #eee; padding-top: 5px; margin-top: 3px; }\n .co-pay { padding: 11px; background: var(--acme-red); color: #fff; border: none; border-radius: 10px; font-size: 13px; font-weight: 700; text-align: center; margin-top: auto; }\n .co-secure { font-size: 9.5px; color: #a39d97; text-align: center; margin-top: 7px; }\n\n /* ── Composer ── */\n .composer { margin-top: auto; padding: 9px 0 2px; border-top: 1px solid #eee; }\n .composer-bar { display: flex; align-items: center; background: #f1efec; border-radius: 20px; padding: 8px 12px; font-size: 11.5px; color: #a39d97; }\n .composer-bar .send { width: 24px; height: 24px; background: var(--acme-dark); border-radius: 50%; margin-left: auto; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; }\n\n /* ── Widget Gallery ── */\n .gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px,1fr)); gap: 22px; }\n .sf { }\n .sf-name { font-size: 12px; font-weight: 700; margin-bottom: 8px; color: #46413c; }\n .sf-name span { font-weight: 400; color: #a39d97; }\n\n /* ── API appendix ── */\n .api-panel { background: #fff; border: 1px solid #e6e3e0; border-radius: 12px; overflow: hidden; margin-bottom: 18px; }\n .api-panel-header { background: var(--acme-dark); color: #fff; padding: 12px 18px; font-size: 13px; font-weight: 700; }\n .api-step { padding: 14px 18px; border-bottom: 1px solid #f1efec; }\n .api-step:last-child { border-bottom: none; }\n .api-step-label { font-size: 10.5px; font-weight: 700; color: #a39d97; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 7px; }\n .api-endpoint { background: #faf8f6; border: 1px solid #eae7e3; border-radius: 8px; padding: 9px 12px; margin-bottom: 7px; }\n .api-endpoint .kind { display: inline-block; font-size: 9px; font-weight: 700; padding: 2px 6px; border-radius: 3px; margin-right: 6px; letter-spacing: 0.4px; font-family: 'JetBrains Mono', monospace; }\n .kind.read { background: var(--green-soft); color: var(--green); }\n .kind.write { background: #ECFEFF; color: #0E7490; }\n .kind.link { background: var(--acme-red-soft); color: var(--acme-red-deep); }\n .api-endpoint .path { font-size: 12.5px; font-weight: 700; color: var(--acme-dark); font-family: 'JetBrains Mono', monospace; }\n .api-endpoint .desc { font-size: 11px; color: #8a8580; margin-top: 3px; }\n .api-note { font-size: 11px; color: #5f594f; background: var(--amber-soft); border-left: 3px solid var(--amber-border); padding: 8px 12px; border-radius: 0 6px 6px 0; margin-top: 6px; }\n .api-note strong { color: #46413c; }\n\n /* ── Audit table ── */\n .audit-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 14px; }\n .audit-table th { text-align: left; padding: 9px 13px; background: #faf8f6; font-weight: 700; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.5px; color: #a39d97; border-bottom: 1px solid #e6e3e0; }\n .audit-table td { padding: 9px 13px; border-bottom: 1px solid #f1efec; vertical-align: top; color: #46413c; }\n .audit-table td:first-child { font-weight: 600; color: #2d2825; }\n .audit-pass { color: var(--green); font-weight: 700; white-space: nowrap; }\n .audit-guard td:first-child { color: var(--acme-red-deep); }\n\n .page-divider { border: none; border-top: 2px solid #e6e3e0; margin: 48px 0; }\n .footer { text-align: center; font-size: 11px; color: #b0aaa4; padding: 24px 0 8px; }\n</style>\n</head>\n<body>\n\n<div class=\"page-header\">\n <h1><span class=\"brand\">Acme Bistro</span> × ChatGPT App — End-to-End Wireframes</h1>\n <p>Prepared by Noodle Seed · Browse → Build order → Confirm → Hand off to pay · Single-restaurant storefront</p>\n <span class=\"scope\">FUNNEL BOUNDARY: menu, order & confirmation IN CHATGPT · <b>PAYMENT ONLY OFF-APP</b> (pay.acme.example) · no per-user auth</span>\n</div>\n\n<div class=\"section-nav\">\n <a href=\"#legend\">Legend</a>\n <a href=\"#flow\">End-to-End Flow</a>\n <a href=\"#build\">Build & Total</a>\n <a href=\"#handoff\">Payment Handoff</a>\n <a href=\"#gallery\">Widget Gallery</a>\n <a href=\"#api\">MCP Tools</a>\n <a href=\"#compliance\">Compliance Audit</a>\n</div>\n\n<div class=\"container\">\n\n <!-- ── LEGEND ── -->\n <div class=\"section\" id=\"legend\">\n <div class=\"vocab\">\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-red\"></div><strong>Acme Red</strong> — primary CTA + logo mark only</div>\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-green\"></div><strong>Green</strong> — trust / \"card never in chat\"</div>\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-amber\"></div><strong>Amber</strong> — off-app payment surface</div>\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-dark\"></div><strong>Dark</strong> — text, user bubbles</div>\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-grey\"></div><strong>Grey</strong> — neutral / host UI</div>\n <div class=\"vocab-item\"><div class=\"vocab-sw sw-dash\"></div><strong>Dashed frame</strong> — off-app destination</div>\n </div>\n <div class=\"rationale\">\n <h4>How to read these wireframes</h4>\n <p><span class=\"r-tag ui\">UI</span> <strong>Solid-border phones are the ChatGPT App</strong> (the Noodle Seed <code>MenuCart</code> widget rendered inline). <strong>Dashed-border phones are off-app</strong> — reached only after the payment hand-off, on Acme's own checkout. Every widget is preceded by its <code>tool-call</code> chip so you can see exactly which tool the model invoked.</p>\n <p><span class=\"r-tag trust\">TRUST</span> <strong>The app never guesses and never touches a card.</strong> Menu, prices, and totals are grounded in the <code>show_menu</code> data; the diner's card is entered only on the amber off-app checkout. This is the whole safety story of the app, and it is visible in the pixels.</p>\n </div>\n </div>\n\n <!-- ═══ SECTION 1: END-TO-END FLOW ═══ -->\n <div class=\"section\" id=\"flow\">\n <span class=\"section-label\">The whole journey</span>\n <div class=\"section-title\">One conversation, from craving to a paid-ready cart</div>\n <div class=\"section-subtitle\">A diner names Acme and their order in plain language; the model renders the menu, reflects the order into a live cart, confirms the total, and mints a signed payment link. Only the final step — entering a card — leaves ChatGPT.</div>\n\n <div class=\"phones-row\">\n\n <!-- Step 1: Menu -->\n <div class=\"phone-step\">\n <div class=\"step-label\">1 · Show the menu<small>show_menu → MenuCart</small></div>\n <div class=\"phone\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"chatgpt-header\"><span class=\"model-name\">ChatGPT</span><span class=\"dots\">···</span></div>\n <div class=\"msg user\">Order me two margheritas and a lemon tart from Acme Bistro.</div>\n <div class=\"tool-call\"><span class=\"icon\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span><span><span class=\"label\">show_menu</span> <span class=\"mono-tool\">{customer:\"Guest\"}</span></span></div>\n <div class=\"msg assistant\">Here's your Acme Bistro order — building it now:</div>\n <div class=\"wcard\">\n <div class=\"wcard-head\">\n <span class=\"logo\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span>\n <div><div class=\"wc-title\">Acme Bistro</div><div class=\"wc-status\">Menu ready. Build the order here; pay at checkout.</div></div>\n <span class=\"wc-chip\">0 in cart</span>\n </div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Stone-baked Margherita</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$14</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Harvest Roast Bowl</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$13</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">House Garden Salad</span> <span class=\"mr-kind\">Starters</span></span><span class=\"mr-price\">$11</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Lemon Tart</span> <span class=\"mr-kind\">Desserts</span></span><span class=\"mr-price\">$8</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Sparkling Water</span> <span class=\"mr-kind\">Drinks</span></span><span class=\"mr-price\">$4</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"wc-foot\"><span class=\"wc-total\">Total <strong>$0</strong></span><button class=\"wc-cta dis\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg>Check out & pay</button></div>\n <div class=\"wc-note\">Payment happens on acme.example — <b>your card is never entered in chat.</b></div>\n </div>\n <div class=\"composer\"><div class=\"composer-bar\">Message<span class=\"send\">↑</span></div></div>\n </div></div>\n </div>\n <div class=\"step-arrow\">→</div>\n\n <!-- Step 2: NL order reflected -->\n <div class=\"phone-step\">\n <div class=\"step-label\">2 · Parse the order<small>add_to_cart ×2 from language</small></div>\n <div class=\"phone\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"chatgpt-header\"><span class=\"model-name\">ChatGPT</span><span class=\"dots\">···</span></div>\n <div class=\"tool-call\"><span class=\"icon\"><svg viewBox=\"0 0 24 24\"><path d=\"M12 5v14M5 12h14\"/></svg></span><span><span class=\"label\">add_to_cart</span> <span class=\"mono-tool\">{item:\"stone_pizza\", quantity:2}</span></span></div>\n <div class=\"tool-call\"><span class=\"icon\"><svg viewBox=\"0 0 24 24\"><path d=\"M12 5v14M5 12h14\"/></svg></span><span><span class=\"label\">add_to_cart</span> <span class=\"mono-tool\">{item:\"lemon_tart\", quantity:1}</span></span></div>\n <div class=\"msg assistant\">Two Stone-baked Margheritas and a Lemon Tart — <strong>$36</strong> so far. Want a drink or a side, or shall I take you to checkout?</div>\n <div class=\"wcard\">\n <div class=\"wcard-head\">\n <span class=\"logo\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span>\n <div><div class=\"wc-title\">Acme Bistro</div><div class=\"wc-status\">Added 2 × Stone-baked Margherita.</div></div>\n <span class=\"wc-chip\">3 in cart</span>\n </div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Stone-baked Margherita</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$14</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">2</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Harvest Roast Bowl</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$13</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">House Garden Salad</span> <span class=\"mr-kind\">Starters</span></span><span class=\"mr-price\">$11</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Lemon Tart</span> <span class=\"mr-kind\">Desserts</span></span><span class=\"mr-price\">$8</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">1</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Sparkling Water</span> <span class=\"mr-kind\">Drinks</span></span><span class=\"mr-price\">$4</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"wc-foot\"><span class=\"wc-total\">Total <strong>$36</strong></span><button class=\"wc-cta\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg>Check out & pay</button></div>\n <div class=\"wc-note\">Payment happens on acme.example — <b>your card is never entered in chat.</b></div>\n </div>\n <div class=\"composer\"><div class=\"composer-bar\">\"add a sparkling water\"<span class=\"send\">↑</span></div></div>\n </div></div>\n </div>\n <div class=\"step-arrow\">→</div>\n\n <!-- Step 3: Edit + confirm total -->\n <div class=\"phone-step\">\n <div class=\"step-label\">3 · Edit & confirm total<small>add / remove · live sum</small></div>\n <div class=\"phone\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"chatgpt-header\"><span class=\"model-name\">ChatGPT</span><span class=\"dots\">···</span></div>\n <div class=\"msg user\">Add a sparkling water too.</div>\n <div class=\"tool-call\"><span class=\"icon\"><svg viewBox=\"0 0 24 24\"><path d=\"M12 5v14M5 12h14\"/></svg></span><span><span class=\"label\">add_to_cart</span> <span class=\"mono-tool\">{item:\"sparkling\", quantity:1}</span></span></div>\n <div class=\"msg assistant\">Added a Sparkling Water — your total is <strong>$40</strong> for four items. Ready to pay whenever you are.</div>\n <div class=\"wcard\">\n <div class=\"wcard-head\">\n <span class=\"logo\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span>\n <div><div class=\"wc-title\">Acme Bistro</div><div class=\"wc-status\">Added Sparkling Water.</div></div>\n <span class=\"wc-chip\">4 in cart</span>\n </div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Stone-baked Margherita</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$14</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">2</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Lemon Tart</span> <span class=\"mr-kind\">Desserts</span></span><span class=\"mr-price\">$8</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">1</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Sparkling Water</span> <span class=\"mr-kind\">Drinks</span></span><span class=\"mr-price\">$4</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">1</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"wc-foot\"><span class=\"wc-total\">Total <strong>$40</strong></span><button class=\"wc-cta\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg>Check out & pay</button></div>\n <div class=\"wc-note\">Payment happens on acme.example — <b>your card is never entered in chat.</b></div>\n </div>\n <div class=\"composer\"><div class=\"composer-bar\">\"check out and pay\"<span class=\"send\">↑</span></div></div>\n </div></div>\n </div>\n <div class=\"step-arrow\">→</div>\n\n <!-- Step 4: Checkout handoff card -->\n <div class=\"phone-step\">\n <div class=\"step-label\">4 · Mint the payment link<small>create_checkout (in-chat)</small></div>\n <div class=\"phone\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"chatgpt-header\"><span class=\"model-name\">ChatGPT</span><span class=\"dots\">···</span></div>\n <div class=\"msg user\">Check out and pay.</div>\n <div class=\"tool-call\"><span class=\"icon\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg></span><span><span class=\"label\">create_checkout</span> <span class=\"mono-tool\">{total:40, cartToken:\"…\"}</span></span></div>\n <div class=\"msg assistant\">Your order's ready — I've opened Acme's secure checkout to take payment:</div>\n <div class=\"handoff-card\">\n <div class=\"glyph\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg></div>\n <h4>Pay on Acme Bistro checkout</h4>\n <p>Your order is staged and ready. Enter your card on <strong>acme.example</strong> — it's never typed in chat.</p>\n <div class=\"mini-sum\">\n <div class=\"row\"><span>2 × Stone-baked Margherita</span><span>$28</span></div>\n <div class=\"row\"><span>1 × Lemon Tart</span><span>$8</span></div>\n <div class=\"row\"><span>1 × Sparkling Water</span><span>$4</span></div>\n <div class=\"row total\"><span>Total</span><span>$40</span></div>\n </div>\n <button class=\"cta-primary\"><svg viewBox=\"0 0 24 24\" style=\"width:14px;height:14px;stroke:#fff;fill:none;stroke-width:2\"><path d=\"M5 12h14M13 6l6 6-6 6\"/></svg>Pay $40 on acme.example ↗</button>\n <button class=\"cta-ghost\">Keep editing</button>\n <div class=\"pay-link\">Opens <code>pay.acme.example/checkout?cart=…&total=40&src=chatgpt</code><br>Signed link · expires in <b>15 min</b> · 🔒 payment handled by Acme</div>\n </div>\n <div class=\"composer\"><div class=\"composer-bar\">Message<span class=\"send\">↑</span></div></div>\n </div></div>\n </div>\n <div class=\"step-arrow\">→</div>\n\n <!-- Step 5: OFF-APP checkout -->\n <div class=\"phone-step\">\n <div class=\"step-label\">5 · Pay (OFF-APP)<small>pay.acme.example · Acme PCI checkout</small></div>\n <div class=\"phone offapp\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"browser-header\"><span class=\"lock\">🔒</span><span class=\"url\">pay.acme.example/checkout?cart=…&total=40&src=chatgpt</span></div>\n <div class=\"checkout-body\">\n <div><div class=\"co-brand\">Acme Bistro</div><div class=\"co-sub\">Secure checkout · order from ChatGPT</div></div>\n <div class=\"co-field\"><div class=\"lbl\">Order</div><div class=\"val\" style=\"font-family:'Inter'\">2 Margherita · 1 Lemon Tart · 1 Sparkling</div></div>\n <div class=\"co-field card\"><div class=\"lbl\">Card number</div><div class=\"val\">•••• •••• •••• ____</div></div>\n <div class=\"co-field\"><div class=\"lbl\">Contact (optional)</div><div class=\"val\" style=\"font-family:'Inter'\">name@example.com</div></div>\n <div style=\"padding:2px 2px 0\">\n <div class=\"co-line\"><span>Subtotal</span><span>$40.00</span></div>\n <div class=\"co-line\"><span>Tax (recomputed by Acme)</span><span>$3.30</span></div>\n <div class=\"co-line total\"><span>Total</span><span>$43.30</span></div>\n </div>\n <button class=\"co-pay\">Pay $43.30</button>\n <div class=\"co-secure\">🔒 PCI-scoped · card stays on acme.example · the ChatGPT App never sees it</div>\n </div>\n </div></div>\n </div>\n\n </div>\n </div>\n\n <hr class=\"page-divider\">\n\n <!-- ═══ SECTION 2: BUILD & TOTAL (deep dive) ═══ -->\n <div class=\"section\" id=\"build\">\n <span class=\"section-label\">Capability · Order building</span>\n <div class=\"section-title\">Build the order in language, confirm it in pixels</div>\n <div class=\"section-subtitle\">The whole menu, the cart, the running total, and the pay button live in one inline widget. The diner drives it two ways — by talking to the model or by tapping the steppers — and both land in the same place.</div>\n\n <div class=\"rationale\">\n <h4>Why this approach</h4>\n <p><span class=\"r-tag acme\">ACME</span> <strong>One kitchen, one widget.</strong> A single restaurant with five items doesn't need discovery, carousels, or a fullscreen menu — it needs the fastest path from craving to a confirmed cart. <code>MenuCart</code> collapses menu, cart, and checkout into one card the diner never scrolls out of.</p>\n <p><span class=\"r-tag ux\">UX</span> <strong>Language is the input; the widget is the receipt.</strong> \"Two margheritas and a lemon tart\" parses into two <code>add_to_cart</code> calls — no tapping required — and the widget instantly shows the result so the diner can trust what the model heard. Editing works the same in both directions.</p>\n <p><span class=\"r-tag ui\">UI</span> <strong>Inline Card, brand accent on the CTA only.</strong> Acme red (<code>#B91C1C</code>) appears only on the header mark and the <strong>Check out & pay</strong> button; everything else uses host theme tokens via cascade layers, so the widget is native in ChatGPT light or dark. No nested scroll — five rows fit.</p>\n <p><span class=\"r-tag trust\">TRUST</span> <strong>The total is honest and display-only.</strong> The widget sums line items live, but the amount that charges is recomputed by Acme at checkout. The reassurance line (\"your card is never entered in chat\") is present in every frame, not just the last one.</p>\n </div>\n </div>\n\n <hr class=\"page-divider\">\n\n <!-- ═══ SECTION 3: PAYMENT HANDOFF ═══ -->\n <div class=\"section\" id=\"handoff\">\n <span class=\"section-label\">Capability · The one handoff</span>\n <div class=\"section-title\">Payment is the only step that leaves ChatGPT</div>\n <div class=\"section-subtitle\">When the order is right, <code>create_checkout</code> mints a signed, expiring link carrying the cart token, the total, and an attribution tag. ChatGPT opens Acme's own PCI checkout. The MCP server is never in the payment path.</div>\n\n <div class=\"rationale\">\n <h4>Why this approach</h4>\n <p><span class=\"r-tag trust\">TRUST</span> <strong>The card never touches the app.</strong> No in-chat card field, no wallet, no stored payment method. Keeping capture on <code>pay.acme.example</code> means the ChatGPT surface adds zero PCI scope — the single most important safety property of the design.</p>\n <p><span class=\"r-tag acme\">ACME</span> <strong>The link is signed, expiring, and attributable.</strong> Acme signs the URL server-side, sets an <code>expires_at</code> (proposed 15 min), and carries <code>src=chatgpt</code> so ChatGPT-sourced revenue is measurable. <code>handoff.allowedDomains</code> lists <code>pay.acme.example</code> and <code>acme.example</code> so the compiler derives the redirect domain and the link opens without a safe-link warning.</p>\n <p><span class=\"r-tag ux\">UX</span> <strong>Context survives the jump.</strong> The cart token (<code>stone_pizzax2-lemon_tartx1-sparklingx1</code>) rehydrates the exact order on Acme's checkout — the diner never re-enters what they already told the model. State is re-validated past the boundary: Acme recomputes pricing and tax and enforces its own total.</p>\n </div>\n\n <div class=\"phones-row\">\n <div class=\"phone-step\">\n <div class=\"step-label\">In-chat: the hand-off card<small>CheckoutHandoff · create_checkout</small></div>\n <div class=\"phone\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"chatgpt-header\"><span class=\"model-name\">ChatGPT</span><span class=\"dots\">···</span></div>\n <div class=\"msg assistant\">Your $40 order is staged. Tap to pay on Acme's checkout:</div>\n <div class=\"handoff-card\">\n <div class=\"glyph\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg></div>\n <h4>Pay on Acme Bistro checkout</h4>\n <p>Sign in (if you like) and pay with your card on <strong>acme.example</strong>.</p>\n <div class=\"mini-sum\">\n <div class=\"row\"><span>4 items</span><span>$40</span></div>\n <div class=\"row total\"><span>Pay on acme.example</span><span>$40+tax</span></div>\n </div>\n <button class=\"cta-primary\">Pay $40 on acme.example ↗</button>\n <div class=\"pay-link\">Signed · <b>expires 15 min</b> · <code>src=chatgpt</code></div>\n </div>\n <div class=\"composer\"><div class=\"composer-bar\">Message<span class=\"send\">↑</span></div></div>\n </div></div>\n </div>\n <div class=\"step-arrow\">→</div>\n <div class=\"phone-step\">\n <div class=\"step-label\">Off-app: Acme checkout<small>dashed = not our app</small></div>\n <div class=\"phone offapp\"><div class=\"phone-notch\"></div><div class=\"phone-screen\">\n <div class=\"browser-header\"><span class=\"lock\">🔒</span><span class=\"url\">pay.acme.example/checkout?cart=stone_pizzax2-lemon_tartx1-sparklingx1&total=40&src=chatgpt</span></div>\n <div class=\"checkout-body\">\n <div><div class=\"co-brand\">Acme Bistro</div><div class=\"co-sub\">Secure checkout</div></div>\n <div class=\"co-field\"><div class=\"lbl\">Order (rehydrated from token)</div><div class=\"val\" style=\"font-family:'Inter'\">2 Margherita · 1 Lemon Tart · 1 Sparkling</div></div>\n <div class=\"co-field card\"><div class=\"lbl\">Card number · PCI-scoped</div><div class=\"val\">•••• •••• •••• ____</div></div>\n <div style=\"padding:2px 2px 0\">\n <div class=\"co-line\"><span>Subtotal (re-validated)</span><span>$40.00</span></div>\n <div class=\"co-line\"><span>Tax</span><span>$3.30</span></div>\n <div class=\"co-line total\"><span>Total</span><span>$43.30</span></div>\n </div>\n <button class=\"co-pay\">Pay $43.30</button>\n <div class=\"co-secure\">🔒 card never leaves acme.example · app has no post-handoff visibility in v1</div>\n </div>\n </div></div>\n </div>\n </div>\n </div>\n\n <hr class=\"page-divider\">\n\n <!-- ═══ WIDGET GALLERY ═══ -->\n <div class=\"section\" id=\"gallery\">\n <span class=\"section-label\">Specimen grid</span>\n <div class=\"section-title\">Widget Gallery</div>\n <div class=\"section-subtitle\">Every widget state rendered once at rest. One widget ships: <code>MenuCart</code>. This is what engineers screenshot against the build.</div>\n\n <div class=\"gallery\">\n <div class=\"sf\">\n <div class=\"sf-name\">MenuCart <span>· empty (CTA disabled)</span></div>\n <div class=\"wcard\">\n <div class=\"wcard-head\"><span class=\"logo\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span><div><div class=\"wc-title\">Acme Bistro</div><div class=\"wc-status\">Build your order, then check out to pay.</div></div><span class=\"wc-chip\">0 in cart</span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Stone-baked Margherita</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$14</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">House Garden Salad</span> <span class=\"mr-kind\">Starters</span></span><span class=\"mr-price\">$11</span><span class=\"mr-qty\"><span class=\"step-btn dis\">−</span><span class=\"mr-count\">0</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"wc-foot\"><span class=\"wc-total\">Total <strong>$0</strong></span><button class=\"wc-cta dis\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg>Check out & pay</button></div>\n <div class=\"wc-note\">Payment happens on acme.example — <b>your card is never entered in chat.</b></div>\n </div>\n </div>\n\n <div class=\"sf\">\n <div class=\"sf-name\">MenuCart <span>· filled ($40, 4 items)</span></div>\n <div class=\"wcard\">\n <div class=\"wcard-head\"><span class=\"logo\"><svg viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"4\"/></svg></span><div><div class=\"wc-title\">Acme Bistro</div><div class=\"wc-status\">Added Sparkling Water.</div></div><span class=\"wc-chip\">4 in cart</span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Stone-baked Margherita</span> <span class=\"mr-kind\">Mains</span></span><span class=\"mr-price\">$14</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">2</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Lemon Tart</span> <span class=\"mr-kind\">Desserts</span></span><span class=\"mr-price\">$8</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">1</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"menu-row\"><span class=\"mr-main\"><span class=\"mr-name\">Sparkling Water</span> <span class=\"mr-kind\">Drinks</span></span><span class=\"mr-price\">$4</span><span class=\"mr-qty\"><span class=\"step-btn on\">−</span><span class=\"mr-count\">1</span><span class=\"step-btn\">+</span></span></div>\n <div class=\"wc-foot\"><span class=\"wc-total\">Total <strong>$40</strong></span><button class=\"wc-cta\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg>Check out & pay</button></div>\n <div class=\"wc-note\">Payment happens on acme.example — <b>your card is never entered in chat.</b></div>\n </div>\n </div>\n\n <div class=\"sf\">\n <div class=\"sf-name\">CheckoutHandoff <span>· signed link (create_checkout)</span></div>\n <div class=\"handoff-card\">\n <div class=\"glyph\"><svg viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M3 10h18\"/></svg></div>\n <h4>Pay on Acme Bistro checkout</h4>\n <p>Your order is staged and ready. Card entered on <strong>acme.example</strong>, never in chat.</p>\n <div class=\"mini-sum\"><div class=\"row\"><span>4 items</span><span>$40</span></div><div class=\"row total\"><span>Total</span><span>$40+tax</span></div></div>\n <button class=\"cta-primary\">Pay $40 on acme.example ↗</button>\n <div class=\"pay-link\">Signed · expires 15 min · <code>src=chatgpt</code></div>\n </div>\n </div>\n </div>\n </div>\n\n <hr class=\"page-divider\">\n\n <!-- ═══ MCP TOOLS APPENDIX ═══ -->\n <div class=\"section\" id=\"api\">\n <span class=\"section-label\">Technical appendix</span>\n <div class=\"section-title\">MCP Tools & Call Sequence</div>\n <div class=\"section-subtitle\">Tool mapping for each wireframe step. Four tools, served by the Noodle Seed–authored <code>acme_bistro</code> server. Concrete request/response JSON lives in <code>api-contract.md</code>.</div>\n\n <div class=\"api-panel\">\n <div class=\"api-panel-header\">Browse & Build (Steps 1–3)</div>\n <div class=\"api-step\">\n <div class=\"api-step-label\">Step 1 — Show the menu</div>\n <div class=\"api-endpoint\"><span class=\"kind read\">READ</span><span class=\"path\">show_menu</span><span class=\"desc\">— tool + view. Returns the 5-item menu and renders MenuCart.</span></div>\n <div class=\"api-note\"><strong>Design intent:</strong> the menu is small and static, so it ships in one read — no pagination, no follow-up call. The <code>items[]</code> array is the model's only source of dish names and prices.</div>\n </div>\n <div class=\"api-step\">\n <div class=\"api-step-label\">Steps 2–3 — Build & edit the cart</div>\n <div class=\"api-endpoint\"><span class=\"kind write\">WRITE</span><span class=\"path\">add_to_cart</span><span class=\"desc\">— tool + app visibility. Reflects a natural-language addition (item + quantity) into the visible cart.</span></div>\n <div class=\"api-endpoint\"><span class=\"kind write\">WRITE</span><span class=\"path\">remove_from_cart</span><span class=\"desc\">— tool + app visibility. Removes one unit; the widget's − button calls the same tool.</span></div>\n <div class=\"api-note\"><strong>Natural-language parsing:</strong> \"two margheritas and a lemon tart\" becomes <code>add_to_cart{item:\"stone_pizza\",quantity:2}</code> + <code>add_to_cart{item:\"lemon_tart\",quantity:1}</code> — the model fills the fields; no UI round-trip. The running total is summed live in the widget (React), not in a tool.</div>\n </div>\n </div>\n\n <div class=\"api-panel\">\n <div class=\"api-panel-header\">Confirm & Hand off (Step 4 → off-app)</div>\n <div class=\"api-step\">\n <div class=\"api-step-label\">Step 4 — Mint the payment link</div>\n <div class=\"api-endpoint\"><span class=\"kind link\">OPEN-LINK</span><span class=\"path\">create_checkout</span><span class=\"desc\">— model-visible tool. Returns a signed deep link + summary; ChatGPT opens it.</span></div>\n <div class=\"api-note\"><strong>No payment in-chat.</strong> The widget passes a url-safe cart token and the numeric total; <code>create_checkout</code> returns <code>checkoutUrl = https://pay.acme.example/checkout?cart=…&total=…&src=chatgpt</code> (production adds a signed <code>expires_at</code>). The app never sees card data. Acme recomputes pricing/tax and enforces the total past the boundary. No <code>submit_order</code> tool exists by design — fulfilment is Acme's.</div>\n </div>\n </div>\n </div>\n\n <hr class=\"page-divider\">\n\n <!-- ═══ COMPLIANCE AUDIT ═══ -->\n <div class=\"section\" id=\"compliance\">\n <span class=\"section-label\">Compliance</span>\n <div class=\"section-title\">OpenAI Apps SDK Compliance Audit</div>\n <div class=\"section-subtitle\">How Acme Bistro maps to OpenAI's published ChatGPT Apps UX Principles and UI Guidelines. Verified in the build with <code>noodle check --target chatgpt</code>.</div>\n\n <div class=\"rationale\" style=\"max-width:100%\">\n <h4>Pre-publishing checklist</h4>\n <table class=\"audit-table\">\n <tr><th style=\"width:32%\">Requirement</th><th>How Acme Bistro addresses it</th><th style=\"width:70px\">Status</th></tr>\n <tr><td>Conversational value — relies on ChatGPT's strengths</td><td>Natural-language ordering (\"two margheritas and a lemon tart\") parses into <code>add_to_cart</code> calls with item + quantity — no tap-driven menu can do this. Editing by sentence (\"drop a margherita\") works the same way.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Beyond base ChatGPT — new knowledge/actions</td><td>Live single-restaurant menu, a running cart, and a signed checkout hand-off to Acme's real payment page. None available in base ChatGPT.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Atomic, model-friendly actions</td><td>Four tools with explicit Zod-typed input/output: <code>show_menu</code> (read), <code>add_to_cart</code>/<code>remove_from_cart</code> (local write), <code>create_checkout</code> (open-link). No ambiguity.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Helpful UI only — would plain text degrade UX?</td><td>Yes for the menu/cart — price rows and steppers are faster to scan than prose, and the running total needs a layout. <strong>No payment widget is built</strong> — card capture is off-platform, so a widget there would be wrong.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Meaningful task completion in-chat</td><td>The full order — browse, build, edit, confirm total — completes in ChatGPT. The one intentional hand-off is payment, on Acme's checkout.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Performance & responsiveness</td><td>One read on entry; cart edits are local to the widget; one link mint at checkout. Static menu keeps <code>show_menu</code> well under target latency.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Discoverability</td><td>Broad natural triggers: \"show me the Acme Bistro menu\", \"order two margheritas from Acme\", \"what's my Acme total?\", \"check out and pay\".</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Platform fit</td><td>Rich prompts (order + quantity in one sentence), multi-turn cart building, in-session memory of the cart. No per-user auth needed for the pre-payment surface.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n </table>\n </div>\n\n <div class=\"rationale\" style=\"max-width:100%;margin-top:18px\">\n <h4>UI guidelines compliance</h4>\n <table class=\"audit-table\">\n <tr><th style=\"width:32%\">Guideline</th><th>Implementation</th><th style=\"width:70px\">Status</th></tr>\n <tr><td>Colour — system tokens; brand only on accents/CTA</td><td>Text, borders, and surfaces use host semantic tokens via cascade layers. Acme red <code>#B91C1C</code> appears only on the header mark and the primary <strong>Check out & pay</strong> button.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Typography — inherit system fonts</td><td>System font stack; no custom Acme typeface inside the widget.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Inline Card — ≤2 actions, no nested scroll, no deep nav</td><td>MenuCart has one primary action (Check out & pay) plus in-card steppers; five rows fit with no inner scroller and no drill-in.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Icons — monochromatic, outlined</td><td>Outlined plate mark and card glyph; no filled brand logo rendered in the response body.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Accessibility — WCAG AA, theme-aware</td><td>Acme red used only as a fill behind light text or as a 1px mark, never as body text on white. Widget adapts to host light/dark via <code>branding.surface</code> / <code>surfaceDark</code>.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr><td>Display mode — correct per intent</td><td>Inline Card only. No Carousel/Fullscreen/PiP — a flat 5-item menu doesn't warrant them.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n </table>\n </div>\n\n <div class=\"rationale\" style=\"max-width:100%;margin-top:18px\">\n <h4>Domain guardrails (Acme-specific trust rows)</h4>\n <table class=\"audit-table\">\n <tr><th style=\"width:32%\">Guardrail</th><th>How Acme Bistro enforces it</th><th style=\"width:70px\">Status</th></tr>\n <tr class=\"audit-guard\"><td>No payment in chat</td><td>No card field, wallet, or stored method anywhere in the app. Card capture happens only on <code>pay.acme.example</code> after the hand-off. The MCP server never enters PCI scope. The \"your card is never entered in chat\" note is in every widget frame.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr class=\"audit-guard\"><td>Allergen / dietary honesty</td><td>The model states only what the menu data holds (name, course, price). Ingredient-level or cross-contact questions are deferred to Acme directly — never \"this is vegetarian/gluten-free\" without a menu flag.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr class=\"audit-guard\"><td>No invented items or prices</td><td>Grounded strictly in the <code>show_menu</code> <code>items[]</code> — only the five dishes, only their listed prices. The total is display-only; Acme recomputes the charge.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr class=\"audit-guard\"><td>Signed, expiring, attributable hand-off</td><td>Checkout link is signed server-side, carries <code>src=chatgpt</code>, and expires (proposed 15 min). <code>handoff.allowedDomains</code> whitelists the redirect domains so the link opens without a safe-link warning.</td><td class=\"audit-pass\">✓ Pass</td></tr>\n <tr class=\"audit-guard\"><td>No unseen fulfilment claims</td><td>The app builds and hands off the order; it makes no pickup-time or order-status promise it can't verify (no post-handoff visibility in v1).</td><td class=\"audit-pass\">✓ Pass</td></tr>\n </table>\n </div>\n </div>\n\n <div class=\"footer\">Acme Bistro (illustrative) · Prepared by Noodle Seed · End-to-end wireframes · matches the runnable <code>acme_bistro</code> server</div>\n\n</div>\n</body>\n</html>\n" },
|
|
@@ -47,7 +48,7 @@ export const BUNDLED_EXAMPLE_FILES = [
|
|
|
47
48
|
{ relPath: "examples/acme-tasks/src/views/widget-style.css", content: ":root {\n color-scheme: light dark;\n font-family:\n Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --nw-bg: #ffffff;\n --nw-surface: #f7f5ff;\n --nw-text: #1c1830;\n --nw-muted: #635d80;\n --nw-border: #e2ddf5;\n --nw-accent: #7c3aed;\n --nw-accent-strong: #6d28d9;\n --nw-accent-soft: #efe9ff;\n --nw-high: #dc2626;\n --nw-medium: #b45309;\n --nw-low: #2563eb;\n --nw-radius: 10px;\n --nw-shadow: 0 18px 50px rgb(30 20 60 / 12%);\n}\n\n.dark,\n[data-theme=\"dark\"] {\n --nw-bg: #161228;\n --nw-surface: #1d1735;\n --nw-text: #f2eeff;\n --nw-muted: #a99fce;\n --nw-border: #342a54;\n --nw-accent: #a78bfa;\n --nw-accent-strong: #8b5cf6;\n --nw-accent-soft: #2a2148;\n --nw-high: #f87171;\n --nw-medium: #fbbf24;\n --nw-low: #60a5fa;\n --nw-shadow: 0 18px 50px rgb(0 0 0 / 32%);\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n background: var(--nw-bg);\n color: var(--nw-text);\n}\n\nbutton,\ninput,\nselect {\n font: inherit;\n}\n\n.nw-shell {\n min-height: 100vh;\n padding: 14px;\n background: var(--nw-bg);\n color: var(--nw-text);\n}\n\n.nw-card {\n max-width: 620px;\n margin: 0 auto;\n background: var(--nw-surface);\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n box-shadow: var(--nw-shadow);\n overflow: hidden;\n}\n\n.nw-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 16px;\n border-bottom: 1px solid var(--nw-border);\n}\n\n.nw-icon svg {\n width: 24px;\n height: 24px;\n fill: none;\n stroke: var(--nw-accent);\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.nw-title-block {\n flex: 1;\n min-width: 0;\n}\n\n.nw-title {\n margin: 0;\n font-size: 17px;\n font-weight: 700;\n}\n\n.nw-subtitle {\n margin: 2px 0 0;\n font-size: 13px;\n color: var(--nw-muted);\n}\n\n.nw-chip {\n padding: 4px 10px;\n border-radius: 999px;\n background: var(--nw-accent-soft);\n color: var(--nw-accent-strong);\n font-size: 12px;\n font-weight: 600;\n}\n\n.nw-capture {\n display: flex;\n gap: 8px;\n padding: 14px 16px 4px;\n}\n\n.nw-input {\n flex: 1;\n padding: 9px 12px;\n border: 1px solid var(--nw-border);\n border-radius: 10px;\n background: var(--nw-bg);\n color: var(--nw-text);\n}\n\n.nw-button {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 9px 14px;\n border: 1px solid var(--nw-border);\n border-radius: 10px;\n background: var(--nw-bg);\n color: var(--nw-text);\n cursor: pointer;\n}\n\n.nw-button-primary {\n background: var(--nw-accent);\n border-color: var(--nw-accent);\n color: #ffffff;\n font-weight: 600;\n}\n\n.nw-button-primary:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.nw-list {\n list-style: none;\n margin: 0;\n padding: 8px 16px 4px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.nw-task {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border: 1px solid var(--nw-border);\n border-radius: 12px;\n background: var(--nw-bg);\n}\n\n.nw-task-title {\n flex: 1;\n min-width: 0;\n}\n\n.nw-task-done {\n opacity: 0.55;\n}\n\n.nw-task-done .nw-task-title {\n text-decoration: line-through;\n}\n\n.nw-check {\n width: 22px;\n height: 22px;\n border: 1.5px solid var(--nw-border);\n border-radius: 999px;\n background: transparent;\n color: #ffffff;\n cursor: pointer;\n flex: none;\n}\n\n.nw-check-on {\n background: var(--nw-accent);\n border-color: var(--nw-accent);\n}\n\n.nw-priority {\n padding: 5px 8px;\n border: 1px solid var(--nw-border);\n border-radius: 8px;\n background: var(--nw-bg);\n color: var(--nw-text);\n font-size: 12px;\n font-weight: 600;\n}\n\n.nw-priority-high {\n color: var(--nw-high);\n}\n\n.nw-priority-medium {\n color: var(--nw-medium);\n}\n\n.nw-priority-low {\n color: var(--nw-low);\n}\n\n.nw-note {\n margin: 0;\n padding: 8px 16px 16px;\n font-size: 12px;\n color: var(--nw-muted);\n}\n" },
|
|
48
49
|
{ relPath: "examples/acme-tasks/test/server.test.ts", content: "import { describe, expect, it } from 'vitest';\nimport app from '../src/server.js';\n\ndescribe('acme-tasks example', () => {\n it('exports a Noodle server definition', () => {\n expect(typeof app.toManifest).toBe('function');\n });\n\n it('exposes a tool for each of the top-3 prioritized flows', async () => {\n // Capture → add_task, Prioritize → list_today (+ set_priority helper), Complete → complete_task.\n const text = JSON.stringify(await app.toManifest());\n expect(text).toContain('add_task');\n expect(text).toContain('list_today');\n expect(text).toContain('complete_task');\n expect(text).toContain('set_priority');\n });\n\n it('seeds today’s list highest-priority first', async () => {\n const text = JSON.stringify(await app.toManifest());\n expect(text).toMatch(/\"tasks\":\\[\\{\"id\":\"email_vendor\".*\"priority\":\"high\"/);\n });\n\n it('opts the conversational completion action into runtime confirmation', async () => {\n const manifest = await app.toManifest();\n const completeTask = manifest.tools.find((candidate) => candidate.name === 'complete_task');\n const addTask = manifest.tools.find((candidate) => candidate.name === 'add_task');\n\n expect(completeTask?.annotations?.confirm).toBe(true);\n expect(addTask?.annotations).not.toHaveProperty('confirm');\n });\n});\n" },
|
|
49
50
|
{ relPath: "examples/acme-tasks/vitest.config.ts", content: "import { defineConfig } from 'vitest/config';\n\n// Local config so `npm test` (vitest run) discovers this example's own tests instead of inheriting a\n// parent monorepo config's include globs.\nexport default defineConfig({\n test: { include: ['test/**/*.test.ts'] },\n});\n" },
|
|
50
|
-
{ relPath: "examples/customer-auth/README.md", content: "# Customer Auth - NoodleSeed.com Firebase customer identity\n\nThis curated example owns the customer/end-user authentication with Firebase bridge capability slot. It is\nthe NoodleSeed.com dogfood app for proving that a SaaS app can protect an MCP endpoint with its own customer\nidentity provider while still using the generic Noodle Seed authoring API.\n\nIt also owns the embedded-assistant showcase: the same authenticated MCP surface can be dropped into the\nSaaS web application as a fully customer-branded assistant with independent light and dark themes. The\nassistant loads the active deployment's instructions and model-visible tools rather than installing a stale\nsecond skill bundle. The standard embedded element also hosts linked MCP Apps behind its sandbox bridge.\n\nThe public developer entrypoint is [`src/server.ts`](src/server.ts). It declares `customerAuth.firebase(...)` with\nthe NoodleSeed.com Firebase project and Firebase Web App public config. It exposes a deliberately small MCP\nsurface for org discovery:\n\n- `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no\n arguments — the org set comes from the verified customer session).\n- `list_org_apps` lists apps for one of those organizations through the dev app API.\n\nThe two tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on, and\n`list_org_apps` takes one of those `org_id`s. There is no NoodleSeed-specific SDK helper. The downstream API\nis an ordinary authored HTTP connector.\n\nDuring MCP OAuth login, Noodle Cloud hosts the Firebase bridge page at\n`https://cloud.noodleseed.dev/oauth/customer/firebase/authorize`. The customer app does not add an\nauthorization route. The SaaS operator only configures Firebase Auth to allow the Noodle Cloud origin, and\nNoodle Cloud signs the customer in with Firebase before posting the Firebase ID token back to its own bridge\ncallback.\n\n## How delegated customer credentials are used\n\nThe example has two declarations that work together:\n\n```ts\nauth: customerAuth.firebase({\n projectId: variable('FIREBASE_PROJECT_ID'),\n apiKey: variable('FIREBASE_WEB_API_KEY'),\n authDomain: variable('FIREBASE_AUTH_DOMAIN'),\n user: {\n id: 'sub',\n email: 'email',\n name: 'name',\n tenant: 'firebase.tenant',\n orgs: 'claims.orgs',\n roles: 'claims.roles',\n },\n}),\n```\n\nThat protects the MCP endpoint with the SaaS customer's Firebase identity. The connector then opts into\ndelegated customer credentials:\n\n```ts\nauth: {\n kind: 'delegatedSessionCookie',\n provider: 'firebase',\n sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,\n tokenField: 'idToken',\n},\n```\n\nTool code calls the connector normally:\n\n```ts\nfulfil({ input, connectors }) {\n const apps = connectors.app_api.listOrgApps({\n org_id: input.org_id,\n skip: input.skip,\n limit: input.limit,\n });\n\n return { result: apps.result };\n}\n```\n\nAt runtime, Noodle Seed verifies the Firebase customer during MCP OAuth, stores that customer's delegated\nFirebase refresh token in the credential broker, and refreshes a short-lived Firebase ID token only when a\nconnector-backed tool calls the NoodleSeed.com API. For this app API, the broker exchanges that ID token at\nthe existing Next.js `/api/auth/session` route and sends the resulting session cookie to the API. The MCP\naccess token remains a Noodle-issued resource-bound token and is never sent to the downstream API.\n\n## Delegated downstream auth for your own API (token exchange)\n\nThe Firebase path above only works for Firebase-session downstreams. When the downstream is **your own\nAPI** with its own token issuance, use `delegatedTokenExchange` instead\n([ADR 0152](../../docs/decisions/0152-delegated-token-exchange-connector-auth.md)): the platform signs a\nshort-lived, JWKS-verifiable assertion of the signed-in user and exchanges it (RFC 8693) at a token\nendpoint you implement, which mints your own user-scoped token — so your API enforces its own per-user\nauthorization on every call. It works for verified direct/federated OIDC, built-in provider identities, and\nembedded assistant sessions, with no per-user OAuth enrollment.\n\n```ts\nauth: {\n kind: 'delegatedTokenExchange',\n tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins\n clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),\n clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),\n scopes: ['time_off'],\n},\n```\n\nYour endpoint authenticates the broker's client credential, verifies the `subject_token` JWT against the\nplatform issuer JWKS (claims include the verified `sub`, `email`, `name`, declared session `claims`,\n`tenant`, and `deployment`), mints a short-lived user-scoped token, and returns the standard\n`{ access_token, token_type, expires_in }` response. The exact wire contract and a copyable endpoint\nimplementation live in [docs/spec/connectors.md](../../docs/spec/connectors.md) and the Agent Kit\nauthoring-workflow reference (\"Delegated downstream auth\"). `noodle auth doctor` reports each declared\nexchange endpoint.\n\n## Validate\n\n```bash\nnoodle auth doctor examples/customer-auth/src/server.ts\nnoodle validate examples/customer-auth/src/server.ts\n```\n\nAgainst a deployed customer-protected environment, set a short-lived real customer token only in\n`NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>`. The live doctor performs\ncredential exchanges without invoking either business tool.\n\n## Run locally\n\n```bash\nnoodle dev examples/customer-auth/src/server.ts --app noodleseed-customer-auth\n```\n\n## Configuration\n\nThe embedded assistant uses a customer-supplied OpenAI Chat Completions-compatible endpoint. Configure its\nmanaged values at the Noodle deployment environment; none of these values belongs in the customer web\napplication environment, and the API key never reaches the browser:\n\n```bash\nnoodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env\nnoodle variables set ASSISTANT_MODEL your-model --scope env\nnoodle secrets set ASSISTANT_MODEL_API_KEY --scope env\nnoodle variables set FIREBASE_PROJECT_ID your-firebase-project --scope env\nnoodle variables set FIREBASE_WEB_API_KEY your-firebase-web-api-key --scope env\nnoodle variables set FIREBASE_AUTH_DOMAIN your-firebase-project.firebaseapp.com --scope env\nnoodle check --target embedded-assistant src/server.ts\n```\n\nAssistant origins are exact. Production embedding origins must use HTTPS; plain HTTP is accepted only for\nloopback development origins such as `http://localhost:3000`, `http://127.0.0.1:3000`, or\n`http://[::1]:3000`. `noodle dev` serves the MCP project, not that separate embedding application.\n\nThe assistant's panel, launcher, status header, composer, and message treatment are configured through the\nbounded `presentation` object in `src/server.ts`. The Atlas-style product treatment is the supported\ncustomization ceiling. These primitives derive their colors from the shared server `branding` block and\nrequire no edits to `@noodleseed/assistant`; raw HTML, CSS, inline SVG, renderer class names, and callbacks\nare intentionally not accepted in deployment configuration.\n\nCreate the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the\nsecret:\n\n```bash\nnoodle assistant clients create --name web --org noodleseed --app customer-auth --env prod\n```\n\nOnly the Noodle service URL, assistant client ID, and assistant client secret belong in the authenticated\ncustomer backend. The model URL, model name, and model API key remain managed by the Noodle deployment.\n\nThe customer's authenticated backend calls `createAssistantSession(...)` from\n`@noodleseed/assistant/server`, passing the already-verified user and browser origin. The browser then uses\nthe returned short-lived session through the Web Component or React wrapper:\n\n```bash\npnpm add @noodleseed/assistant\n```\n\n```tsx\nimport { NoodleAssistant } from '@noodleseed/assistant/react';\n\n<NoodleAssistant\n sessionEndpoint=\"/api/noodle-assistant/session\"\n theme=\"auto\"\n onSessionExpired={() => console.info('Assistant session renewed')}\n/>;\n```\n\n`theme=\"auto\"` follows the SaaS application. The server-level `branding` block is inherited by both MCP App\nwidgets and the assistant; documented `--ns-assistant-*` semantic CSS variables remain the final integration\nescape hatch. There is no second assistant branding declaration.\nThe end-user UI contains only customer branding.\nText streams progressively. Expired turns re-exchange through the authenticated backend and retry once;\nconsent-bound tool confirmations never replay automatically.\n\nThe Firebase project ID is required because Firebase ID tokens use the project ID as the token audience and\nissuer suffix. The runtime verifies `aud` against the project ID and `iss` against\n`https://securetoken.google.com/<projectId>`.\n\nThe Firebase Web API key and auth domain are public Firebase browser configuration. They let the Noodle\nCloud-hosted bridge initialize Firebase Auth for this customer project; they are not server secrets. Keep\nthem out of source with `variable(...)`, restrict the Firebase key to the expected browser origins and APIs,\nand use `secret(...)` only for credentials that must never reach a browser.\n\nThe `noodleseed_app_api` connector currently points at the NoodleSeed.com dev app surface:\n\n```text\nhttps://dev.noodleseed.com\n```\n\nWhen the customer app moves from `dev.noodleseed.com` to `app.noodleseed.com`, update the connector's\n`noodleseedApiOrigin` constant to the production API origin that serves the same paths.\n\nThe connector uses delegated Firebase customer credentials. During the customer OAuth bridge, Noodle Seed\nverifies the Firebase ID token, stores the Firebase refresh token through the credential broker, and refreshes\na Firebase ID token when the connector calls the NoodleSeed.com app API. The broker then exchanges that ID\ntoken for the app's existing Next.js session cookie. There is no shared `NOODLESEED_APP_API_TOKEN` for this\nexample.\n\nFirebase Auth must list `cloud.noodleseed.dev` as an authorized domain before browser sign-in works in\nproduction.\n\n## Deploy customer-protected to Noodle Seed Cloud\n\n```bash\nnoodle deploy examples/customer-auth/src/server.ts \\\n --org noodleseed \\\n --app customer-auth \\\n --env prod \\\n --access customers\n```\n\nEndpoint:\n\n```text\nhttps://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp\n```\n\n## MCP Primitives\n\n- Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in\n customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.\n- Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.\n\n## Auth boundary\n\nFirebase ID-token verification is handled by Noodle Seed's generic Firebase bridge adapter during OAuth\nissuance. The MCP client receives a Noodle-issued, resource-bound access token marked as a Firebase customer\nidentity; raw Firebase tokens and inbound MCP bearer tokens are never forwarded to tools, connectors,\nwidgets, or downstream systems.\n\nThe connector-backed tools use the credential broker to turn the signed-in Firebase customer session into the\nsame session-cookie credential that the existing NoodleSeed.com Next.js API already expects. The inbound MCP\nbearer token is never used as an app API credential.\n" },
|
|
51
|
+
{ relPath: "examples/customer-auth/README.md", content: "# Customer Auth - NoodleSeed.com Firebase customer identity\n\nThis curated example owns the customer/end-user authentication with Firebase bridge capability slot. It is\nthe NoodleSeed.com dogfood app for proving that a SaaS app can protect an MCP endpoint with its own customer\nidentity provider while still using the generic Noodle Seed authoring API.\n\nIt also owns the embedded-assistant showcase: the same authenticated MCP surface can be dropped into the\nSaaS web application as a fully customer-branded assistant with independent light and dark themes. The\nassistant loads the active deployment's instructions and model-visible tools rather than installing a stale\nsecond skill bundle. The standard embedded element also hosts linked MCP Apps behind its sandbox bridge.\n\nThe public developer entrypoint is [`src/server.ts`](src/server.ts). It declares `customerAuth.firebase(...)` with\nthe NoodleSeed.com Firebase project and Firebase Web App public config. It exposes a deliberately small MCP\nsurface for org discovery:\n\n- `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no\n arguments — the org set comes from the verified customer session).\n- `list_org_apps` lists apps for one of those organizations through the dev app API.\n\nThe two tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on, and\n`list_org_apps` takes one of those `org_id`s. There is no NoodleSeed-specific SDK helper. The downstream API\nis an ordinary authored HTTP connector.\n\nDuring MCP OAuth login, Noodle Cloud hosts the Firebase bridge page at\n`https://cloud.noodleseed.dev/oauth/customer/firebase/authorize`. The customer app does not add an\nauthorization route. The SaaS operator only configures Firebase Auth to allow the Noodle Cloud origin, and\nNoodle Cloud signs the customer in with Firebase before posting the Firebase ID token back to its own bridge\ncallback.\n\n## How delegated customer credentials are used\n\nThis flagship uses the legacy Firebase provider bridge because it is runnable today. That is distinct from\nCore-v2 account-selecting connector aliases: those declare catalog `credentialProfiles` and bind each alias\nwith `bind(connector, { profile, connection })`. Hosted bound `managedSecret(...)` connections are runnable;\nbound `externalExchange()` and `clientCredentials(...)` currently compile as portable metadata but fail\nclosed before secret or network access until their provider slice lands. Do not replace this example's\nworking delegated-session-cookie path with either unsupported bound exchange source.\n\nThe example has two declarations that work together:\n\n```ts\nauth: customerAuth.firebase({\n projectId: variable('FIREBASE_PROJECT_ID'),\n apiKey: variable('FIREBASE_WEB_API_KEY'),\n authDomain: variable('FIREBASE_AUTH_DOMAIN'),\n user: {\n id: 'sub',\n email: 'email',\n name: 'name',\n tenant: 'firebase.tenant',\n orgs: 'claims.orgs',\n roles: 'claims.roles',\n },\n}),\n```\n\nThat protects the MCP endpoint with the SaaS customer's Firebase identity. The connector then opts into\ndelegated customer credentials:\n\n```ts\nauth: {\n kind: 'delegatedSessionCookie',\n provider: 'firebase',\n sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,\n tokenField: 'idToken',\n},\n```\n\nTool code calls the connector normally:\n\n```ts\nfulfil({ input, connectors }) {\n const apps = connectors.app_api.listOrgApps({\n org_id: input.org_id,\n skip: input.skip,\n limit: input.limit,\n });\n\n return { result: apps.result };\n}\n```\n\nAt runtime, Noodle Seed verifies the Firebase customer during MCP OAuth, stores that customer's delegated\nFirebase refresh token in the credential broker, and refreshes a short-lived Firebase ID token only when a\nconnector-backed tool calls the NoodleSeed.com API. For this app API, the broker exchanges that ID token at\nthe existing Next.js `/api/auth/session` route and sends the resulting session cookie to the API. The MCP\naccess token remains a Noodle-issued resource-bound token and is never sent to the downstream API.\n\n## Delegated downstream auth for your own API (token exchange)\n\nThe Firebase path above only works for Firebase-session downstreams. When the downstream is **your own\nAPI** with its own token issuance, use `delegatedTokenExchange` instead\n([ADR 0152](../../docs/decisions/0152-delegated-token-exchange-connector-auth.md)): the platform signs a\nshort-lived, JWKS-verifiable assertion of the signed-in user and exchanges it (RFC 8693) at a token\nendpoint you implement, which mints your own user-scoped token — so your API enforces its own per-user\nauthorization on every call. It works for verified direct/federated OIDC, built-in provider identities, and\nembedded assistant sessions, with no per-user OAuth enrollment.\n\n```ts\nauth: {\n kind: 'delegatedTokenExchange',\n tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins\n clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),\n clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),\n scopes: ['time_off'],\n},\n```\n\nYour endpoint authenticates the broker's client credential, verifies the `subject_token` JWT against the\nplatform issuer JWKS (claims include the verified `sub`, `email`, `name`, declared session `claims`,\n`tenant`, and `deployment`), mints a short-lived user-scoped token, and returns the standard\n`{ access_token, token_type, expires_in }` response. The exact wire contract and a copyable endpoint\nimplementation live in [docs/spec/connectors.md](../../docs/spec/connectors.md) and the Agent Kit\nauthoring-workflow reference (\"Delegated downstream auth\"). `noodle auth doctor` reports each declared\nexchange endpoint.\n\n## Validate\n\n```bash\nnoodle auth doctor examples/customer-auth/src/server.ts\nnoodle validate examples/customer-auth/src/server.ts\n```\n\nAgainst a deployed customer-protected environment, set a short-lived real customer token only in\n`NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>`. The live doctor performs\ncredential exchanges without invoking either business tool.\n\n## Run locally\n\n```bash\nnoodle dev examples/customer-auth/src/server.ts --app noodleseed-customer-auth\n```\n\n## Configuration\n\nThe embedded assistant uses a customer-supplied OpenAI Chat Completions-compatible endpoint. Configure its\nmanaged values at the Noodle deployment environment; none of these values belongs in the customer web\napplication environment, and the API key never reaches the browser:\n\n```bash\nnoodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env\nnoodle variables set ASSISTANT_MODEL your-model --scope env\nnoodle secrets set ASSISTANT_MODEL_API_KEY --scope env\nnoodle variables set FIREBASE_PROJECT_ID your-firebase-project --scope env\nnoodle variables set FIREBASE_WEB_API_KEY your-firebase-web-api-key --scope env\nnoodle variables set FIREBASE_AUTH_DOMAIN your-firebase-project.firebaseapp.com --scope env\nnoodle check --target embedded-assistant src/server.ts\n```\n\nAssistant origins are exact. Production embedding origins must use HTTPS; plain HTTP is accepted only for\nloopback development origins such as `http://localhost:3000`, `http://127.0.0.1:3000`, or\n`http://[::1]:3000`. `noodle dev` serves the MCP project, not that separate embedding application.\n\nThe assistant's panel, launcher, status header, composer, and message treatment are configured through the\nbounded `presentation` object in `src/server.ts`. The Atlas-style product treatment is the supported\ncustomization ceiling. These primitives derive their colors from the shared server `branding` block and\nrequire no edits to `@noodleseed/assistant`; raw HTML, CSS, inline SVG, renderer class names, and callbacks\nare intentionally not accepted in deployment configuration.\n\nCreate the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the\nsecret:\n\n```bash\nnoodle assistant clients create --name web --org noodleseed --app customer-auth --env prod\n```\n\nOnly the Noodle service URL, assistant client ID, and assistant client secret belong in the authenticated\ncustomer backend. The model URL, model name, and model API key remain managed by the Noodle deployment.\n\nThe customer's authenticated backend calls `createAssistantSession(...)` from\n`@noodleseed/assistant/server`, passing the already-verified user and browser origin. The browser then uses\nthe returned short-lived session through the Web Component or React wrapper:\n\n```bash\npnpm add @noodleseed/assistant\n```\n\n```tsx\nimport { NoodleAssistant } from '@noodleseed/assistant/react';\n\n<NoodleAssistant\n sessionEndpoint=\"/api/noodle-assistant/session\"\n theme=\"auto\"\n onSessionExpired={() => console.info('Assistant session renewed')}\n/>;\n```\n\n`theme=\"auto\"` follows the SaaS application. The server-level `branding` block is inherited by both MCP App\nwidgets and the assistant; documented `--ns-assistant-*` semantic CSS variables remain the final integration\nescape hatch. There is no second assistant branding declaration.\nThe end-user UI contains only customer branding.\nText streams progressively. Expired turns re-exchange through the authenticated backend and retry once;\nconsent-bound tool confirmations never replay automatically.\n\nThe Firebase project ID is required because Firebase ID tokens use the project ID as the token audience and\nissuer suffix. The runtime verifies `aud` against the project ID and `iss` against\n`https://securetoken.google.com/<projectId>`.\n\nThe Firebase Web API key and auth domain are public Firebase browser configuration. They let the Noodle\nCloud-hosted bridge initialize Firebase Auth for this customer project; they are not server secrets. Keep\nthem out of source with `variable(...)`, restrict the Firebase key to the expected browser origins and APIs,\nand use `secret(...)` only for credentials that must never reach a browser.\n\nThe `noodleseed_app_api` connector currently points at the NoodleSeed.com dev app surface:\n\n```text\nhttps://dev.noodleseed.com\n```\n\nWhen the customer app moves from `dev.noodleseed.com` to `app.noodleseed.com`, update the connector's\n`noodleseedApiOrigin` constant to the production API origin that serves the same paths.\n\nThe connector uses delegated Firebase customer credentials. During the customer OAuth bridge, Noodle Seed\nverifies the Firebase ID token, stores the Firebase refresh token through the credential broker, and refreshes\na Firebase ID token when the connector calls the NoodleSeed.com app API. The broker then exchanges that ID\ntoken for the app's existing Next.js session cookie. There is no shared `NOODLESEED_APP_API_TOKEN` for this\nexample.\n\nFirebase Auth must list `cloud.noodleseed.dev` as an authorized domain before browser sign-in works in\nproduction.\n\n## Deploy customer-protected to Noodle Seed Cloud\n\n```bash\nnoodle deploy examples/customer-auth/src/server.ts \\\n --org noodleseed \\\n --app customer-auth \\\n --env prod \\\n --access customers\n```\n\nEndpoint:\n\n```text\nhttps://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp\n```\n\n## MCP Primitives\n\n- Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in\n customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.\n- Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.\n\n## Auth boundary\n\nFirebase ID-token verification is handled by Noodle Seed's generic Firebase bridge adapter during OAuth\nissuance. The MCP client receives a Noodle-issued, resource-bound access token marked as a Firebase customer\nidentity; raw Firebase tokens and inbound MCP bearer tokens are never forwarded to tools, connectors,\nwidgets, or downstream systems.\n\nThe connector-backed tools use the credential broker to turn the signed-in Firebase customer session into the\nsame session-cookie credential that the existing NoodleSeed.com Next.js API already expects. The inbound MCP\nbearer token is never used as an app API credential.\n" },
|
|
51
52
|
{ relPath: "examples/customer-auth/noodle.json", content: "{\n \"entrypoint\": \"src/server.ts\",\n \"name\": \"customer-auth\"\n}\n" },
|
|
52
53
|
{ relPath: "examples/customer-auth/package.json", content: "{\n \"name\": \"customer-auth\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"test\": \"vitest run\",\n \"validate\": \"noodle validate\",\n \"dev\": \"noodle dev\",\n \"deploy\": \"noodle deploy\"\n },\n \"devDependencies\": {\n \"@noodleseed/one\": \"latest\",\n \"vitest\": \"latest\"\n }\n}\n" },
|
|
53
54
|
{ relPath: "examples/customer-auth/src/server.ts", content: "import {\n annotations,\n connector,\n customerAuth,\n embeddedAssistant,\n openAICompatible,\n secret,\n server,\n tool,\n variable,\n z,\n} from '@noodleseed/one';\n\nconst noodleseedApiOrigin = 'https://dev.noodleseed.com';\n\nconst noodleseedApi = connector('noodleseed_app_api')\n .version('1.0.0')\n .http({\n baseUrl: noodleseedApiOrigin,\n allowedOrigins: [noodleseedApiOrigin],\n auth: {\n kind: 'delegatedSessionCookie',\n provider: 'firebase',\n sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,\n tokenField: 'idToken',\n },\n operations: {\n list_org_apps: {\n type: 'read',\n method: 'GET',\n path: '/api/organizations/${args.org_id}/apps',\n query: ['skip', 'limit'],\n input: z.object({\n org_id: z.string(),\n skip: z.number().optional(),\n limit: z.number().optional(),\n }),\n output: z.object({ result: z.unknown().optional() }),\n response: {\n result: '${response}',\n },\n },\n list_organizations: {\n type: 'read',\n method: 'GET',\n path: '/api/organizations',\n output: z.object({ organizations: z.array(z.unknown()).optional() }),\n response: {\n organizations: '${response.organizations}',\n },\n },\n },\n });\n\nexport default server(\n 'noodleseed_customer_auth',\n {\n title: 'NoodleSeed.com Customer Auth',\n version: '1.0.0',\n branding: {\n name: 'Noodle Seed Assistant',\n accent: '#E85D24',\n surface: '#FFFFFF',\n surfaceDark: '#171310',\n colorScheme: 'auto',\n theme: {\n light: { accentText: '#FFFFFF', text: '#1C1714' },\n dark: { accent: '#FF8A4C', accentText: '#1C100A', text: '#FFF8F2' },\n },\n },\n use: { app_api: noodleseedApi },\n auth: customerAuth.firebase({\n projectId: variable('FIREBASE_PROJECT_ID'),\n apiKey: variable('FIREBASE_WEB_API_KEY'),\n authDomain: variable('FIREBASE_AUTH_DOMAIN'),\n user: {\n id: 'sub',\n email: 'email',\n name: 'name',\n tenant: 'firebase.tenant',\n orgs: 'claims.orgs',\n roles: 'claims.roles',\n },\n }),\n instructions:\n 'Customer-authenticated demo. Firebase proves the customer identity, while read-only NoodleSeed.com API calls use broker-managed delegated Firebase customer credentials.',\n assistant: embeddedAssistant({\n model: openAICompatible({\n baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),\n model: variable('ASSISTANT_MODEL'),\n apiKey: secret('ASSISTANT_MODEL_API_KEY'),\n }),\n // Production origins are exact HTTPS; http://localhost:<port> is allowed for local development.\n allowedOrigins: [\n 'https://app.noodleseed.com',\n 'https://dev.noodleseed.com',\n 'http://localhost:3000',\n ],\n layout: { mode: 'floating', position: 'bottom-right', panelWidth: 420 },\n labels: {\n welcomeHeading: 'How can I help with Noodle Seed?',\n composerPlaceholder: 'Ask about your apps…',\n },\n presentation: {\n panel: { surface: 'glass', elevation: 'soft', border: 'subtle' },\n launcher: { icon: 'brand-mark', status: 'session', effect: 'pulse' },\n header: {\n mark: 'status',\n badge: { text: 'Workspace online', tone: 'success', indicator: true },\n },\n composer: { leadingIcon: 'brand-mark', shape: 'pill' },\n },\n suggestedPrompts: ['Show my organizations', 'List the apps in my organization'],\n }),\n },\n [\n tool('list_org_apps', {\n title: 'List organization apps',\n description: 'List NoodleSeed.com apps for an organization from the dev app API.',\n input: z.object({\n org_id: z.string().meta({ title: 'Organization' }),\n skip: z.number().int().min(0).optional().meta({ title: 'Starting item' }),\n limit: z.number().int().min(1).max(100).optional().meta({ title: 'Maximum results' }),\n }),\n output: z.object({\n result: z.unknown(),\n }),\n annotations: annotations.readOnly(),\n fulfil({ input, connectors }) {\n const apps = connectors.app_api.listOrgApps({\n org_id: input.org_id,\n skip: input.skip,\n limit: input.limit,\n });\n return {\n result: apps.result,\n };\n },\n }),\n tool('list_my_organizations', {\n title: 'List my organizations',\n description: 'List the NoodleSeed.com organizations the signed-in customer belongs to.',\n contextProvider: true,\n input: z.object({}),\n output: z.object({\n organizations: z.array(z.unknown()),\n }),\n annotations: annotations.readOnly(),\n fulfil({ connectors }) {\n const organizations = connectors.app_api.listOrganizations();\n return {\n organizations: organizations.organizations,\n };\n },\n }),\n ],\n);\n" },
|
|
@@ -62,6 +63,12 @@ export const BUNDLED_EXAMPLE_FILES = [
|
|
|
62
63
|
{ relPath: "examples/food-ordering/src/views/widget-style.css", content: ":root {\n color-scheme: light dark;\n font-family:\n Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --nw-bg: #ffffff;\n --nw-surface: #f7f7f5;\n --nw-surface-strong: #ffffff;\n --nw-text: #1f2328;\n --nw-muted: #677079;\n --nw-border: #d9dee3;\n --nw-accent: #0f8f5f;\n --nw-accent-strong: #047857;\n --nw-accent-soft: #e8f7ef;\n --nw-info: #1d6feb;\n --nw-info-soft: #eaf2ff;\n --nw-warn: #b7791f;\n --nw-danger: #c2410c;\n --nw-shadow: 0 18px 50px rgb(15 23 42 / 12%);\n --nw-radius: 8px;\n}\n\n.dark,\n[data-theme=\"dark\"] {\n --nw-bg: #111820;\n --nw-surface: #16212a;\n --nw-surface-strong: #101820;\n --nw-text: #f4f7f8;\n --nw-muted: #a4b0ba;\n --nw-border: #33414c;\n --nw-accent: #25c385;\n --nw-accent-strong: #19a974;\n --nw-accent-soft: #123a2b;\n --nw-info: #4c9aff;\n --nw-info-soft: #132b4d;\n --nw-warn: #f0b35b;\n --nw-danger: #fb7b54;\n --nw-shadow: 0 18px 50px rgb(0 0 0 / 28%);\n}\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n margin: 0;\n background: var(--nw-bg);\n color: var(--nw-text);\n}\n\nbutton,\ninput,\nselect {\n font: inherit;\n}\n\n.nw-shell {\n min-height: 100vh;\n padding: 14px;\n background: var(--nw-bg);\n color: var(--nw-text);\n}\n\n.nw-card {\n width: min(100%, 560px);\n margin: 0 auto;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-surface-strong);\n box-shadow: var(--nw-shadow);\n overflow: hidden;\n}\n\n.nw-card-wide {\n width: min(100%, 720px);\n}\n\n.nsr-card {\n width: min(100%, 560px);\n margin: 0 auto;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-surface-strong);\n box-shadow: var(--nw-shadow);\n overflow: hidden;\n}\n\n.nsr-card-wide {\n width: min(100%, 720px);\n}\n\n.nw-header {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 14px;\n border-bottom: 1px solid var(--nw-border);\n}\n\n.nsr-header {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 14px;\n border-bottom: 1px solid var(--nw-border);\n}\n\n.nw-icon {\n display: inline-grid;\n width: 32px;\n height: 32px;\n flex: 0 0 auto;\n place-items: center;\n border-radius: 8px;\n background: linear-gradient(145deg, var(--nw-accent), var(--nw-accent-strong));\n color: white;\n}\n\n.nsr-icon {\n display: inline-grid;\n width: 32px;\n height: 32px;\n flex: 0 0 auto;\n place-items: center;\n border-radius: 8px;\n background: linear-gradient(145deg, var(--nw-accent), var(--nw-accent-strong));\n color: white;\n}\n\n.nw-icon svg,\n.nsr-icon svg,\n.nw-button svg,\n.nw-meta svg {\n width: 16px;\n height: 16px;\n stroke: currentColor;\n stroke-width: 2;\n fill: none;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.nw-title-block {\n min-width: 0;\n flex: 1;\n}\n\n.nsr-title-block {\n min-width: 0;\n flex: 1;\n}\n\n.nw-title {\n margin: 0;\n font-size: 16px;\n line-height: 1.2;\n font-weight: 700;\n letter-spacing: 0;\n}\n\n.nsr-title {\n margin: 0;\n font-size: 16px;\n line-height: 1.2;\n font-weight: 700;\n letter-spacing: 0;\n}\n\n.nw-subtitle {\n margin: 4px 0 0;\n color: var(--nw-muted);\n font-size: 12px;\n line-height: 1.35;\n}\n\n.nsr-subtitle {\n margin: 4px 0 0;\n color: var(--nw-muted);\n font-size: 12px;\n line-height: 1.35;\n}\n\n.nw-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 3px 9px;\n border: 1px solid var(--nw-border);\n border-radius: 999px;\n background: var(--nw-surface);\n color: var(--nw-text);\n font-size: 12px;\n font-weight: 600;\n white-space: nowrap;\n}\n\n.nsr-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 3px 9px;\n border: 1px solid var(--nw-border);\n border-radius: 999px;\n background: var(--nw-surface);\n color: var(--nw-text);\n font-size: 12px;\n font-weight: 600;\n white-space: nowrap;\n}\n\n.nw-chip::before {\n width: 7px;\n height: 7px;\n border-radius: 99px;\n background: var(--nw-accent);\n content: \"\";\n}\n\n.nsr-chip::before {\n width: 7px;\n height: 7px;\n border-radius: 99px;\n background: var(--nw-accent);\n content: \"\";\n}\n\n.nw-tabs {\n display: grid;\n grid-template-columns: repeat(5, minmax(0, 1fr));\n gap: 1px;\n border-bottom: 1px solid var(--nw-border);\n background: var(--nw-border);\n}\n\n.nsr-tabs {\n display: grid;\n grid-template-columns: repeat(5, minmax(0, 1fr));\n gap: 1px;\n border-bottom: 1px solid var(--nw-border);\n background: var(--nw-border);\n}\n\n.nw-tab {\n min-width: 0;\n min-height: 34px;\n border: 0;\n background: var(--nw-surface-strong);\n color: var(--nw-muted);\n font-size: 12px;\n font-weight: 700;\n text-transform: capitalize;\n cursor: pointer;\n}\n\n.nsr-tab {\n min-width: 0;\n min-height: 34px;\n border: 0;\n background: var(--nw-surface-strong);\n color: var(--nw-muted);\n font-size: 12px;\n font-weight: 700;\n text-transform: capitalize;\n cursor: pointer;\n}\n\n.nw-tab[aria-current=\"step\"] {\n color: var(--nw-text);\n background: var(--nw-accent-soft);\n}\n\n.nsr-tab[aria-current=\"step\"] {\n color: var(--nw-text);\n background: var(--nw-accent-soft);\n}\n\n.nw-body {\n display: grid;\n gap: 14px;\n padding: 14px;\n}\n\n.nw-grid {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(180px, 0.75fr);\n gap: 14px;\n}\n\n.nw-section-title {\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 700;\n}\n\n.nw-section-head {\n display: grid;\n gap: 2px;\n}\n\n.nw-section-detail {\n margin: 0;\n color: var(--nw-muted);\n font-size: 12px;\n}\n\n.nw-menu-list,\n.nw-summary-list,\n.nw-feature-list {\n display: grid;\n gap: 8px;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.nw-menu-item {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 10px;\n width: 100%;\n padding: 10px;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-bg);\n color: var(--nw-text);\n text-align: left;\n cursor: pointer;\n}\n\n.nw-menu-item[aria-pressed=\"true\"] {\n border-color: var(--nw-accent);\n background: var(--nw-accent-soft);\n}\n\n.nw-menu-name,\n.nw-feature-name {\n display: block;\n font-size: 13px;\n font-weight: 700;\n}\n\n.nw-menu-desc,\n.nw-feature-desc {\n display: block;\n margin-top: 3px;\n color: var(--nw-muted);\n font-size: 12px;\n line-height: 1.35;\n}\n\n.nw-price {\n align-self: center;\n color: var(--nw-text);\n font-size: 13px;\n font-weight: 700;\n}\n\n.nw-status {\n align-self: center;\n color: var(--nw-muted);\n font-size: 12px;\n font-weight: 800;\n white-space: nowrap;\n}\n\n.nw-status-ok {\n color: var(--nw-accent-strong);\n}\n\n.nw-field-grid {\n display: grid;\n grid-template-columns: 1fr 120px;\n gap: 10px;\n}\n\n.nw-field {\n display: grid;\n gap: 5px;\n color: var(--nw-muted);\n font-size: 12px;\n font-weight: 650;\n}\n\n.nsr-field-label {\n color: var(--nw-muted);\n}\n\n.nsr-stepper {\n display: grid;\n grid-template-columns: 34px minmax(42px, 1fr) 34px;\n width: 100%;\n min-height: 34px;\n border: 1px solid var(--nw-border);\n border-radius: 7px;\n overflow: hidden;\n background: var(--nw-bg);\n}\n\n.nsr-stepper button {\n border: 0;\n background: var(--nw-surface);\n color: var(--nw-text);\n cursor: pointer;\n}\n\n.nsr-stepper button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.nsr-stepper-value {\n display: grid;\n place-items: center;\n color: var(--nw-text);\n font-size: 13px;\n font-weight: 800;\n}\n\n.nw-input,\n.nw-select {\n min-width: 0;\n width: 100%;\n min-height: 34px;\n padding: 7px 9px;\n border: 1px solid var(--nw-border);\n border-radius: 7px;\n background: var(--nw-bg);\n color: var(--nw-text);\n outline: none;\n}\n\n.nw-input:focus,\n.nw-select:focus,\n.nw-button:focus-visible,\n.nw-menu-item:focus-visible {\n border-color: var(--nw-info);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--nw-info) 24%, transparent);\n}\n\n.nw-modifier-grid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 8px;\n}\n\n.nw-option {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 9px;\n border: 1px solid var(--nw-border);\n border-radius: 7px;\n background: var(--nw-bg);\n color: var(--nw-text);\n font-size: 12px;\n font-weight: 650;\n}\n\n.nsr-choice {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 9px;\n border: 1px solid var(--nw-border);\n border-radius: 7px;\n background: var(--nw-bg);\n color: var(--nw-text);\n font-size: 12px;\n font-weight: 650;\n}\n\n.nw-summary {\n display: grid;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-surface);\n}\n\n.nw-summary-compact {\n align-self: end;\n}\n\n.nw-summary-row {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n padding-bottom: 8px;\n border-bottom: 1px solid color-mix(in srgb, var(--nw-border) 70%, transparent);\n}\n\n.nw-summary-row:last-child {\n padding-bottom: 0;\n border-bottom: 0;\n}\n\n.nw-summary-row dt {\n color: var(--nw-muted);\n font-size: 12px;\n}\n\n.nw-line-note {\n display: block;\n margin-top: 2px;\n font-size: 11px;\n font-weight: 500;\n}\n\n.nw-summary-row dd {\n margin: 0;\n text-align: right;\n font-size: 13px;\n font-weight: 700;\n}\n\n.nw-total dd {\n font-size: 18px;\n}\n\n.nw-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.nsr-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.nw-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n min-height: 36px;\n padding: 8px 12px;\n border: 1px solid var(--nw-border);\n border-radius: 7px;\n background: var(--nw-bg);\n color: var(--nw-text);\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n}\n\n.nw-button-primary {\n border-color: var(--nw-accent-strong);\n background: linear-gradient(145deg, var(--nw-accent), var(--nw-accent-strong));\n color: white;\n}\n\n.nw-button-info {\n border-color: var(--nw-info);\n background: var(--nw-info);\n color: white;\n}\n\n.nw-button:disabled {\n cursor: wait;\n opacity: 0.65;\n}\n\n.nw-note {\n margin: 0;\n padding: 10px;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-surface);\n color: var(--nw-muted);\n font-size: 12px;\n line-height: 1.45;\n}\n\n.nw-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--nw-border);\n color: var(--nw-muted);\n font-size: 12px;\n}\n\n.nsr-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 14px;\n border-top: 1px solid var(--nw-border);\n color: var(--nw-muted);\n font-size: 12px;\n}\n\n.nw-meta {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.nw-feature {\n display: grid;\n grid-template-columns: 24px minmax(0, 1fr);\n gap: 10px;\n padding: 10px;\n border: 1px solid var(--nw-border);\n border-radius: var(--nw-radius);\n background: var(--nw-bg);\n}\n\n.nw-check {\n display: inline-grid;\n width: 22px;\n height: 22px;\n place-items: center;\n border-radius: 6px;\n background: var(--nw-accent);\n color: white;\n font-size: 13px;\n font-weight: 800;\n}\n\n.nw-check svg {\n width: 14px;\n height: 14px;\n stroke: currentColor;\n stroke-width: 3;\n fill: none;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n@media (max-width: 560px) {\n .nw-shell {\n padding: 10px;\n }\n\n .nw-grid,\n .nw-field-grid,\n .nw-modifier-grid {\n grid-template-columns: 1fr;\n }\n\n .nw-tabs {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n\n .nsr-tabs {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n }\n\n .nw-header {\n align-items: flex-start;\n }\n\n .nsr-header {\n align-items: flex-start;\n }\n\n .nw-chip {\n max-width: 120px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .nsr-chip {\n max-width: 120px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .nw-button {\n flex: 1 1 140px;\n }\n}\n" },
|
|
63
64
|
{ relPath: "examples/food-ordering/test/server.test.ts", content: "import { describe, expect, it } from 'vitest';\nimport app from '../src/server.js';\n\ndescribe('food-ordering example', () => {\n it('exports a Noodle server definition', () => {\n expect(typeof app.toManifest).toBe('function');\n });\n\n it('emits a complete ordering app manifest with cart state and app-only helpers', async () => {\n const manifest = (await app.toManifest()) as {\n server: {\n name: string;\n context?: {\n defaults?: { locale?: string; timeZone?: string };\n ambient?: {\n outputSchema?: unknown;\n fulfilment?: { output?: unknown };\n };\n };\n };\n handoff?: { allowedDomains?: string[] };\n state?: { handles?: Record<string, { kind: string; scope: string }> };\n connectors?: Record<string, { id: string; version: string }>;\n tools: Array<{\n name: string;\n visibility?: string[];\n annotations?: Record<string, unknown>;\n output?: unknown;\n fulfilment?: { steps?: unknown[]; output?: unknown };\n }>;\n widgets?: Array<{\n name: string;\n tool: string;\n view?: { component?: string; entry?: string };\n }>;\n };\n\n expect(manifest.server.name).toBe('food_ordering');\n expect(manifest.server.context).toMatchObject({\n defaults: { locale: 'en-US', timeZone: 'America/New_York' },\n ambient: {\n fulfilment: {\n output: {\n serviceArea: 'Harbor District',\n orderingDate: '${context.temporal.localDate}',\n },\n },\n },\n });\n expect(manifest.state?.handles?.cart).toMatchObject({\n kind: 'cart',\n scope: 'caller',\n });\n expect(manifest.handoff?.allowedDomains).toEqual(['https://orders.example.com']);\n expect(manifest.connectors?.state).toEqual({ id: 'noodle_state', version: '1.0.0' });\n\n const tools = new Map(manifest.tools.map((tool) => [tool.name, tool]));\n expect(manifest.widgets?.find((widget) => widget.tool === 'open_ordering')?.view).toMatchObject(\n {\n component: 'ordering-flow',\n entry: './views/ordering-flow.tsx',\n },\n );\n for (const helper of [\n 'search_stores',\n 'load_menu',\n 'load_item',\n 'read_cart',\n 'sync_cart',\n 'prepare_checkout',\n ]) {\n expect(tools.get(helper)?.visibility).toEqual(['app']);\n }\n expect(JSON.stringify(tools.get('open_ordering'))).toContain('featuredItems');\n expect(JSON.stringify(tools.get('open_ordering'))).toContain('${context.temporal.localDate}');\n expect(JSON.stringify(tools.get('open_ordering'))).toContain('${context.ambient.serviceArea}');\n expect(JSON.stringify(tools.get('sync_cart'))).toContain('revision');\n expect(tools.get('sync_cart')?.annotations?.confirm).toBe(false);\n expect(tools.get('prepare_checkout')?.annotations?.confirm).toBe(false);\n expect(tools.get('plan_order')?.fulfilment).toMatchObject({\n steps: [\n {\n id: 'choose_fulfilment',\n elicit: {\n message: 'How should we fulfil this order?',\n requestedSchema: {\n type: 'object',\n properties: {\n method: { type: 'string', enum: ['pickup', 'delivery'] },\n requestedDate: { type: 'string', format: 'date' },\n },\n required: ['method', 'requestedDate'],\n },\n },\n },\n ],\n output: {\n method: '${steps.choose_fulfilment.method}',\n requestedDate: '${steps.choose_fulfilment.requestedDate}',\n },\n });\n expect(manifest.widgets?.map((widget) => widget.name)).toContain('capabilities_card');\n });\n});\n" },
|
|
64
65
|
{ relPath: "examples/food-ordering/vitest.config.ts", content: "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n resolve: {\n alias: {\n '@noodleseed/one': new URL('../../packages/authoring/src/index.ts', import.meta.url).pathname,\n '@noodle-borg/capabilities': new URL(\n '../../packages/capabilities/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/compiler': new URL('../../packages/compiler/src/index.ts', import.meta.url)\n .pathname,\n '@noodle-borg/compute': new URL('../../packages/compute/src/index.ts', import.meta.url)\n .pathname,\n '@noodle-borg/connector-defs': new URL(\n '../../packages/connector-defs/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/connector-http': new URL(\n '../../packages/connector-http/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/runtime': new URL('../../packages/runtime/src/index.ts', import.meta.url)\n .pathname,\n },\n },\n test: {\n include: ['test/**/*.test.ts'],\n },\n});\n" },
|
|
66
|
+
{ relPath: "examples/gmail-multi-account/README.md", content: "# Gmail multi-account automation\n\n**Owns:** The flagship proof that one reusable connector can be bound to multiple independently\nauthenticated accounts inside one MCP server.\n\nThis fictional example binds `gmailConnector()` twice through separate `externalExchange()` logical\nconnections. Public tools always accept `accounts: [...]`; reads accept either account or the canonical\npersonal-then-work pair, while mutations accept exactly one account and require runtime confirmation.\n\nCapability slot: **reusable connector + independently authenticated multi-account bindings**. It is distinct\nfrom `customer-auth`, which owns authentication of the MCP caller rather than downstream connector accounts.\n\nThe labels `personal@example.com` and `work@example.com` are static display labels, not provider identities.\nThe deployment-owned credential provider maps each logical connection to its real Google authorization.\nNo Google client, provider account id, token, or real email address belongs in this project.\n\n## Safety and API boundary\n\n- Search, message/thread reads, draft reads, and vacation-setting reads may target one or both accounts.\n- Draft creation/update/send, label changes, archive, raw send, trash, and vacation updates target one account.\n- Every mutation is prepared against the exact selected binding and must be confirmed before dispatch.\n- `send_message.raw` and draft `raw` are RFC 2822 MIME bytes encoded with base64url. This example does not\n pretend that `to`/`subject`/`body` strings are sufficient to encode Unicode MIME correctly.\n- Vacation `startTime`/`endTime` schemas enforce only digit-shaped 1–19 character epoch-millisecond strings.\n When both are supplied, Gmail's backend remains authoritative for the required `startTime < endTime`\n relationship; this example does not claim cross-field JSON Schema validation.\n- Trash is reversible. Permanent message/thread/draft deletion, delegation, forwarding/sharing settings,\n and unrestricted raw HTTP requests are intentionally absent.\n\n## Local checks\n\n```sh\nnoodle validate\nnoodle test\n```\n\nThe committed tests compile hermetic fake connector responses. They never contact Gmail or load OAuth\ncredentials. A real deployment additionally needs an operator-provided external credential exchange\nendpoint for each logical connection.\n\n## Personal automation skill\n\nThe source skill is [`skills/personal-email-automation/SKILL.md`](skills/personal-email-automation/SKILL.md).\nValidate it with the standard skill validator before distribution. The source skill is shipped as part of\nthis example; canonical export of an app and its skill as an installable Codex plugin remains roadmap work\nand is not currently provided by Noodle Seed.\n" },
|
|
67
|
+
{ relPath: "examples/gmail-multi-account/noodle.json", content: "{\n \"entrypoint\": \"src/server.ts\",\n \"name\": \"gmail-multi-account\"\n}\n" },
|
|
68
|
+
{ relPath: "examples/gmail-multi-account/package.json", content: "{\n \"name\": \"gmail-multi-account\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"test\": \"vitest run\",\n \"validate\": \"noodle validate\",\n \"dev\": \"noodle dev\",\n \"deploy\": \"noodle deploy\"\n },\n \"devDependencies\": {\n \"@noodleseed/one\": \"latest\",\n \"vitest\": \"latest\"\n }\n}\n" },
|
|
69
|
+
{ relPath: "examples/gmail-multi-account/src/server.ts", content: "import {\n annotations,\n bind,\n connection,\n connector,\n externalExchange,\n gmailConnector,\n server,\n tool,\n when,\n z,\n} from '@noodleseed/one';\n\nexport const PERSONAL_ACCOUNT = 'personal@example.com';\nexport const WORK_ACCOUNT = 'work@example.com';\n\nconst readAccounts = z.union([\n z.tuple([z.literal(PERSONAL_ACCOUNT)]).and(z.array(z.literal(PERSONAL_ACCOUNT)).length(1)),\n z.tuple([z.literal(WORK_ACCOUNT)]).and(z.array(z.literal(WORK_ACCOUNT)).length(1)),\n z\n .tuple([z.literal(PERSONAL_ACCOUNT), z.literal(WORK_ACCOUNT)])\n .and(z.array(z.enum([PERSONAL_ACCOUNT, WORK_ACCOUNT])).length(2)),\n]);\nconst writeAccounts = z.union([\n z.tuple([z.literal(PERSONAL_ACCOUNT)]).and(z.array(z.literal(PERSONAL_ACCOUNT)).length(1)),\n z.tuple([z.literal(WORK_ACCOUNT)]).and(z.array(z.literal(WORK_ACCOUNT)).length(1)),\n]);\nconst identifier = z.string().min(1);\nconst rawMessage = z.string().min(1);\nconst labelIds = z.array(identifier).min(1).max(100);\nconst epochMillis = z.string().regex(/^\\d{1,19}$/);\nconst vacationOptionalFields = {\n restrict_to_contacts: z.boolean().optional(),\n restrict_to_domain: z.boolean().optional(),\n start_time: epochMillis.optional(),\n end_time: epochMillis.optional(),\n};\nconst output = z.object({\n results: z.array(\n z.object({ account: z.enum([PERSONAL_ACCOUNT, WORK_ACCOUNT]), data: z.unknown() }),\n ),\n});\n\nconst gmail = gmailConnector();\nconst personal = connection('personal_gmail', externalExchange());\nconst work = connection('work_gmail', externalExchange());\n\nconst merge = connector('gmail_account_results')\n .version('1.0.0')\n .compute('merge', {\n type: 'read',\n input: z.object({\n personal: z.unknown().optional(),\n work_first: z.unknown().optional(),\n work_second: z.unknown().optional(),\n }),\n output,\n run: (input) => {\n const results: Array<{ account: string; data: unknown }> = [];\n if (input.personal !== undefined) {\n results.push({ account: 'personal@example.com', data: input.personal });\n }\n const workData = input.work_first ?? input.work_second;\n if (workData !== undefined) {\n results.push({ account: 'work@example.com', data: workData });\n }\n return { results };\n },\n });\n\nconst confirmedWrite = annotations.openAction({ destructive: false, confirm: true });\nconst confirmedTrash = annotations.openAction({ destructive: true, confirm: true });\n\nexport default server(\n 'gmail_multi_account',\n {\n title: 'Gmail Multi-Account Automation',\n version: '1.0.0',\n use: {\n personal_gmail: bind(gmail, { profile: 'user_oauth', connection: personal }),\n work_gmail: bind(gmail, { profile: 'user_oauth', connection: work }),\n gmail_results: merge,\n },\n },\n [\n tool('search_messages', {\n description: 'Search one connected Gmail account or the canonical personal-and-work pair.',\n input: z.object({\n accounts: readAccounts,\n query: z.string(),\n max_results: z.number().int().min(1).max(500).optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.search_messages({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.search_messages({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.search_messages({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('get_message', {\n description: 'Get one Gmail message from one connected account or both canonical accounts.',\n input: z.object({\n accounts: readAccounts,\n message_id: identifier,\n format: z.enum(['minimal', 'full', 'raw', 'metadata']).optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.get_message({\n message_id: input.message_id,\n format: input.format,\n }),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_message({\n message_id: input.message_id,\n format: input.format,\n }),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_message({\n message_id: input.message_id,\n format: input.format,\n }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('get_thread', {\n description: 'Get one Gmail thread from one connected account or both canonical accounts.',\n input: z.object({\n accounts: readAccounts,\n thread_id: identifier,\n format: z.enum(['minimal', 'full', 'metadata']).optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.get_thread({\n thread_id: input.thread_id,\n format: input.format,\n }),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_thread({\n thread_id: input.thread_id,\n format: input.format,\n }),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_thread({\n thread_id: input.thread_id,\n format: input.format,\n }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('list_drafts', {\n description: 'List drafts from one connected Gmail account or both canonical accounts.',\n input: z.object({\n accounts: readAccounts,\n query: z.string().optional(),\n max_results: z.number().int().min(1).max(500).optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.list_drafts({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.list_drafts({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.list_drafts({\n q: input.query,\n maxResults: input.max_results,\n }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('get_draft', {\n description: 'Get one draft from one connected Gmail account or both canonical accounts.',\n input: z.object({\n accounts: readAccounts,\n draft_id: identifier,\n format: z.enum(['minimal', 'full', 'raw', 'metadata']).optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.get_draft({\n draft_id: input.draft_id,\n format: input.format,\n }),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_draft({\n draft_id: input.draft_id,\n format: input.format,\n }),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_draft({\n draft_id: input.draft_id,\n format: input.format,\n }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('get_vacation', {\n description: 'Read vacation-responder settings from one connected account or both accounts.',\n input: z.object({ accounts: readAccounts }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.get_vacation({}),\n );\n const workFirst = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_vacation({}),\n );\n const workSecond = when(input.accounts.at(1).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.get_vacation({}),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: workFirst.data.optional(),\n work_second: workSecond.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('create_draft', {\n description:\n 'Create a Gmail draft in exactly one selected account from a base64url MIME message.',\n annotations: confirmedWrite,\n input: z.object({ accounts: writeAccounts, raw: rawMessage }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.create_draft({ raw: input.raw }),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.create_draft({ raw: input.raw }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('update_draft', {\n description:\n 'Replace a Gmail draft in exactly one selected account with a base64url MIME message.',\n annotations: confirmedWrite,\n input: z.object({ accounts: writeAccounts, draft_id: identifier, raw: rawMessage }),\n output,\n fulfil: ({ input, connectors }) => {\n const args = { draft_id: input.draft_id, raw: input.raw };\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.update_draft(args),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.update_draft(args),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('send_draft', {\n description: 'Send an existing Gmail draft from exactly one selected account.',\n annotations: confirmedWrite,\n input: z.object({ accounts: writeAccounts, draft_id: identifier }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.send_draft({ draft_id: input.draft_id }),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.send_draft({ draft_id: input.draft_id }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('modify_message_labels', {\n description: 'Add or remove Gmail label ids on one message in exactly one selected account.',\n annotations: confirmedWrite,\n input: z.object({\n accounts: writeAccounts,\n message_id: identifier,\n add_label_ids: labelIds.optional(),\n remove_label_ids: labelIds.optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const args = {\n message_id: input.message_id,\n addLabelIds: input.add_label_ids,\n removeLabelIds: input.remove_label_ids,\n };\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.modify_message_labels(args),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.modify_message_labels(args),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('archive_message', {\n description: 'Archive one Gmail message in exactly one account by removing INBOX.',\n annotations: confirmedWrite,\n input: z.object({ accounts: writeAccounts, message_id: identifier }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.archive_message({ message_id: input.message_id }),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.archive_message({ message_id: input.message_id }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('send_message', {\n description: 'Send one base64url MIME message from exactly one selected Gmail account.',\n annotations: confirmedWrite,\n input: z.object({\n accounts: writeAccounts,\n raw: rawMessage,\n thread_id: identifier.optional(),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const args = { raw: input.raw, threadId: input.thread_id };\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.send_message(args),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.send_message(args),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('trash_message', {\n description: 'Move one Gmail message to trash in exactly one account. This is reversible.',\n annotations: confirmedTrash,\n input: z.object({ accounts: writeAccounts, message_id: identifier }),\n output,\n fulfil: ({ input, connectors }) => {\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.trash_message({ message_id: input.message_id }),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.trash_message({ message_id: input.message_id }),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n tool('update_vacation', {\n description: 'Update vacation-responder settings on exactly one selected Gmail account.',\n annotations: confirmedWrite,\n input: z.object({\n accounts: writeAccounts,\n settings: z.union([\n z.object({\n enable_auto_reply: z.literal(false),\n response_subject: z.string().min(1).optional(),\n response_body_plain_text: z.string().min(1).optional(),\n ...vacationOptionalFields,\n }),\n z.object({\n enable_auto_reply: z.literal(true),\n response_subject: z.string().min(1),\n response_body_plain_text: z.string().min(1).optional(),\n ...vacationOptionalFields,\n }),\n z.object({\n enable_auto_reply: z.literal(true),\n response_subject: z.string().min(1).optional(),\n response_body_plain_text: z.string().min(1),\n ...vacationOptionalFields,\n }),\n ]),\n }),\n output,\n fulfil: ({ input, connectors }) => {\n const args = {\n enableAutoReply: input.settings.enable_auto_reply,\n responseSubject: input.settings.response_subject,\n responseBodyPlainText: input.settings.response_body_plain_text,\n restrictToContacts: input.settings.restrict_to_contacts,\n restrictToDomain: input.settings.restrict_to_domain,\n startTime: input.settings.start_time,\n endTime: input.settings.end_time,\n };\n const personal = when(input.accounts.at(0).equals(PERSONAL_ACCOUNT), () =>\n connectors.personal_gmail.update_vacation(args),\n );\n const work = when(input.accounts.at(0).equals(WORK_ACCOUNT), () =>\n connectors.work_gmail.update_vacation(args),\n );\n const merged = connectors.gmail_results.merge({\n personal: personal.data.optional(),\n work_first: work.data.optional(),\n });\n return { results: merged.results };\n },\n }),\n ],\n);\n" },
|
|
70
|
+
{ relPath: "examples/gmail-multi-account/test/server.test.ts", content: "import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { describe, expect, it } from 'vitest';\nimport {\n compileManifest,\n InMemoryCatalog,\n validateJsonSchemaWithDefaults,\n} from '../../../packages/compiler/src/index.js';\nimport { compileConnectors } from '../../../packages/connector-defs/src/index.js';\nimport {\n type CredentialBroker,\n type CredentialRequest,\n type DownstreamCredential,\n executePreparedTool,\n executeTool,\n InMemoryConnectorRegistry,\n isConfirmationRequired,\n prepareToolForConfirmation,\n} from '../../../packages/runtime/src/index.js';\nimport app, { PERSONAL_ACCOUNT, WORK_ACCOUNT } from '../src/server.js';\n\nconst here = dirname(fileURLToPath(import.meta.url));\n\nasync function compiledFixture() {\n const catalog = structuredClone(app.toConnectorCatalog());\n if (catalog === undefined) throw new Error('expected connector catalog');\n for (const connector of catalog.connectors) {\n if ('http' in connector) {\n for (const [name, operation] of Object.entries(connector.operations)) {\n operation.fake = {\n response: {\n id: `${name}-fixture`,\n messages: [{ id: `${name}-message` }],\n drafts: [{ id: `${name}-draft` }],\n },\n };\n }\n }\n }\n const connectors = compileConnectors(JSON.stringify(catalog), { mode: 'fake' });\n if (!connectors.ok) throw new Error(JSON.stringify(connectors.errors));\n const manifest = await app.toManifest();\n const compiled = compileManifest(manifest, {\n catalog: new InMemoryCatalog(connectors.catalog),\n });\n if (!compiled.ok) throw new Error(JSON.stringify(compiled.errors));\n const requests: CredentialRequest[] = [];\n const broker: CredentialBroker = {\n getCredential(request): Promise<DownstreamCredential> {\n requests.push(request);\n return Promise.resolve({ token: `${request.bindingId}-fixture-token` });\n },\n };\n return {\n artifact: compiled.artifact,\n deps: { connectors: new InMemoryConnectorRegistry(connectors.connectors), broker },\n requests,\n };\n}\n\ndescribe('multi-account Gmail flagship', () => {\n it('binds the same curated connector twice without provider ids, credentials, or real labels', async () => {\n const manifest = await app.toManifest();\n expect(manifest.connectors).toMatchObject({\n personal_gmail: {\n id: 'gmail',\n binding: {\n profile: 'user_oauth',\n connection: { id: 'personal_gmail', source: { kind: 'externalExchange' } },\n },\n },\n work_gmail: {\n id: 'gmail',\n binding: {\n profile: 'user_oauth',\n connection: { id: 'work_gmail', source: { kind: 'externalExchange' } },\n },\n },\n });\n const wire = JSON.stringify({ manifest, catalog: app.toConnectorCatalog() });\n expect(wire).not.toMatch(/access[_-]?token|client[_-]?secret/i);\n });\n\n it('emits enforceable canonical account arrays on every tool', async () => {\n const manifest = await app.toManifest();\n for (const tool of manifest.tools) {\n expect(tool.inputSchema.properties).toHaveProperty('accounts');\n expect(JSON.stringify(tool.inputSchema.properties.accounts)).toContain(PERSONAL_ACCOUNT);\n expect(JSON.stringify(tool.inputSchema.properties.accounts)).toContain(WORK_ACCOUNT);\n }\n const read = manifest.tools.find((tool) => tool.name === 'search_messages');\n const write = manifest.tools.find((tool) => tool.name === 'trash_message');\n const readAccountsSchema = JSON.stringify(read?.inputSchema.properties.accounts);\n const writeAccountsSchema = JSON.stringify(write?.inputSchema.properties.accounts);\n expect(readAccountsSchema).toContain(PERSONAL_ACCOUNT);\n expect(readAccountsSchema).toContain(WORK_ACCOUNT);\n expect(readAccountsSchema).toContain('\"maxItems\":2');\n expect(writeAccountsSchema).not.toContain('\"maxItems\":2');\n });\n\n it('allows subject-only or plain-body-only vacation replies but rejects an empty enabled reply', async () => {\n const manifest = await app.toManifest();\n const schema = manifest.tools.find((tool) => tool.name === 'update_vacation')?.inputSchema;\n if (schema === undefined) throw new Error('expected update_vacation schema');\n const base = { accounts: [PERSONAL_ACCOUNT], settings: { enable_auto_reply: true } };\n\n expect(\n validateJsonSchemaWithDefaults(schema, {\n ...base,\n settings: { ...base.settings, response_subject: 'Away' },\n }).issues,\n ).toHaveLength(0);\n expect(\n validateJsonSchemaWithDefaults(schema, {\n ...base,\n settings: { ...base.settings, response_body_plain_text: 'Back soon' },\n }).issues,\n ).toHaveLength(0);\n expect(validateJsonSchemaWithDefaults(schema, base).issues).not.toHaveLength(0);\n });\n\n it.each([\n [],\n ['unknown@example.com'],\n [PERSONAL_ACCOUNT, PERSONAL_ACCOUNT],\n [WORK_ACCOUNT, PERSONAL_ACCOUNT],\n ])('rejects invalid read accounts %j before connector dispatch', async (accounts) => {\n const setup = await compiledFixture();\n const schema = setup.artifact.tools.find(\n (candidate) => candidate.name === 'search_messages',\n )?.inputSchema;\n if (!schema) throw new Error('expected search schema');\n const result = validateJsonSchemaWithDefaults(schema, { accounts, query: '' });\n expect(result.issues).not.toHaveLength(0);\n expect(setup.requests).toHaveLength(0);\n });\n\n it('rejects multi-account mutations at the write schema boundary', async () => {\n const setup = await compiledFixture();\n const schema = setup.artifact.tools.find(\n (candidate) => candidate.name === 'trash_message',\n )?.inputSchema;\n if (!schema) throw new Error('expected trash schema');\n\n const result = validateJsonSchemaWithDefaults(schema, {\n accounts: [PERSONAL_ACCOUNT, WORK_ACCOUNT],\n message_id: 'fixture-message',\n });\n\n expect(result.issues).not.toHaveLength(0);\n expect(setup.requests).toHaveLength(0);\n });\n\n it('rejects a no-op label mutation before confirmation or credential lookup', async () => {\n const setup = await compiledFixture();\n\n await expect(\n prepareToolForConfirmation(\n setup.artifact,\n 'modify_message_labels',\n { accounts: [PERSONAL_ACCOUNT], message_id: 'fixture-message' },\n setup.deps,\n ),\n ).resolves.toMatchObject({ status: 'failed', error: { code: 'arg_invalid' } });\n expect(setup.requests).toHaveLength(0);\n });\n\n it('routes personal, work, and combined reads to isolated bindings and preserves account labels', async () => {\n const personal = await compiledFixture();\n const personalResult = await executeTool(\n personal.artifact,\n 'search_messages',\n { accounts: [PERSONAL_ACCOUNT], query: 'is:unread' },\n personal.deps,\n );\n expect(personal.requests.flatMap((request) => request.bindingId ?? [])).toEqual([\n 'personal_gmail',\n ]);\n expect(personalResult).toMatchObject({\n ok: true,\n output: { results: [{ account: PERSONAL_ACCOUNT }] },\n });\n\n const work = await compiledFixture();\n const workResult = await executeTool(\n work.artifact,\n 'search_messages',\n { accounts: [WORK_ACCOUNT], query: 'is:unread' },\n work.deps,\n );\n expect(work.requests.flatMap((request) => request.bindingId ?? [])).toEqual(['work_gmail']);\n expect(workResult).toMatchObject({\n ok: true,\n output: { results: [{ account: WORK_ACCOUNT }] },\n });\n\n const both = await compiledFixture();\n const bothResult = await executeTool(\n both.artifact,\n 'search_messages',\n { accounts: [PERSONAL_ACCOUNT, WORK_ACCOUNT], query: 'is:unread' },\n both.deps,\n );\n expect(both.requests.flatMap((request) => request.bindingId ?? [])).toEqual([\n 'personal_gmail',\n 'work_gmail',\n ]);\n expect(bothResult).toMatchObject({\n ok: true,\n output: {\n results: [{ account: PERSONAL_ACCOUNT }, { account: WORK_ACCOUNT }],\n },\n });\n });\n\n it('prepares the exact selected write binding, dispatches only after confirmation, and rejects replay drift', async () => {\n const setup = await compiledFixture();\n const prepared = await prepareToolForConfirmation(\n setup.artifact,\n 'trash_message',\n { accounts: [WORK_ACCOUNT], message_id: 'fixture-message' },\n setup.deps,\n );\n expect(isConfirmationRequired(prepared)).toBe(true);\n if (!isConfirmationRequired(prepared)) throw new Error('expected confirmation');\n expect(setup.requests).toHaveLength(0);\n expect(prepared.review.action).toMatchObject({\n bindingId: 'work_gmail',\n connectionId: 'work_gmail',\n operation: 'trash_message',\n });\n\n await expect(\n executePreparedTool(setup.artifact, prepared.continuation, setup.deps),\n ).resolves.toMatchObject({ status: 'completed' });\n expect(setup.requests.flatMap((request) => request.bindingId ?? [])).toEqual(['work_gmail']);\n\n const personalPrepared = structuredClone(prepared.continuation);\n if (personalPrepared.reviewedAction) {\n (personalPrepared.reviewedAction as { bindingId?: string }).bindingId = 'personal_gmail';\n }\n await expect(\n executePreparedTool(setup.artifact, personalPrepared, setup.deps),\n ).resolves.toMatchObject({ status: 'failed' });\n });\n\n it('makes every mutation confirmable and keeps the personal-email skill concise and safe', async () => {\n const manifest = await app.toManifest();\n const catalog = app.toConnectorCatalog();\n const actionNames = new Set(\n catalog?.connectors.flatMap((connector) =>\n Object.entries(connector.operations)\n .filter(([, operation]) => operation.type === 'action')\n .map(([name]) => name),\n ),\n );\n for (const tool of manifest.tools.filter((candidate) => actionNames.has(candidate.name))) {\n expect(tool.annotations?.confirm, tool.name).toBe(true);\n }\n\n const skill = readFileSync(join(here, '../skills/personal-email-automation/SKILL.md'), 'utf8');\n expect(skill).toMatch(/^---\\nname: personal-email-automation\\ndescription:/);\n expect(skill.split('\\n').length).toBeLessThan(140);\n expect(skill).toContain('accounts');\n expect(skill).toMatch(/draft/i);\n expect(skill).toMatch(/confirm/i);\n expect(skill).toMatch(/permanent delete/i);\n expect(skill).not.toMatch(/client[_-]?secret|access[_-]?token/i);\n });\n});\n" },
|
|
71
|
+
{ relPath: "examples/gmail-multi-account/vitest.config.ts", content: "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n resolve: {\n alias: {\n '@noodleseed/one': new URL('../../packages/authoring/src/index.ts', import.meta.url).pathname,\n '@noodle-borg/capabilities': new URL(\n '../../packages/capabilities/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/compiler': new URL('../../packages/compiler/src/index.ts', import.meta.url)\n .pathname,\n '@noodle-borg/compute': new URL('../../packages/compute/src/index.ts', import.meta.url)\n .pathname,\n '@noodle-borg/connector-defs': new URL(\n '../../packages/connector-defs/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/connector-http': new URL(\n '../../packages/connector-http/src/index.ts',\n import.meta.url,\n ).pathname,\n '@noodle-borg/runtime': new URL('../../packages/runtime/src/index.ts', import.meta.url)\n .pathname,\n },\n },\n test: { include: ['test/**/*.test.ts'] },\n});\n" },
|
|
65
72
|
{ relPath: "examples/hello/README.md", content: "# hello — minimal TypeScript quickstart\n\nThe smallest deployable Noodle app: a single `greet` tool authored in TypeScript with no\nconnectors, secrets, flows, widgets, or handoff policy. It still uses the current server options form\nso new authors see where server-level branding belongs. Use it to smoke the author loop\n(`noodle validate` / `noodle dev`) or a first deploy.\n\nWhen an installed Noodle Developer plugin drives this example, its skill runs these logical\n`noodle` commands through the plugin-managed, version-pinned launcher and isolated host profile.\nDo not install or update a global CLI: the coding agent writes and tests this source while Noodle\nguides and operates the validate, preview, deploy, inspect, and debug workflow.\n\n```sh\nnoodle dev examples/hello/src/server.ts --app hello\nnoodle deploy examples/hello/src/server.ts --org acme --app hello\n```\n\n`noodle export manifest examples/hello/src/server.ts` compiles the same entrypoint locally and prints\nthe portable, vendor-neutral manifest JSON — the eject path: your `server.ts` plus this manifest is\nthe whole app, yours to read, diff, and keep.\n\nIt is also the fixture for `pnpm smoke:dev` and the e2e harness, so keep its tool surface stable.\n" },
|
|
66
73
|
{ relPath: "examples/hello/noodle.json", content: "{\n \"entrypoint\": \"src/server.ts\",\n \"name\": \"hello\",\n \"template\": \"hello\"\n}\n" },
|
|
67
74
|
{ relPath: "examples/hello/package.json", content: "{\n \"name\": \"hello\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"test\": \"vitest run\",\n \"validate\": \"noodle validate\",\n \"dev\": \"noodle dev\",\n \"deploy\": \"noodle deploy\"\n },\n \"devDependencies\": {\n \"@noodleseed/one\": \"latest\",\n \"vitest\": \"latest\"\n }\n}\n" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"example-files.js","sourceRoot":"","sources":["../../src/generated/example-files.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,kFAAkF;AAClF,iGAAiG;AAQjG,uGAAuG;AACvG,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,OAAO;IACP,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,YAAY;IACZ,aAAa;IACb,eAAe;
|
|
1
|
+
{"version":3,"file":"example-files.js","sourceRoot":"","sources":["../../src/generated/example-files.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,kFAAkF;AAClF,iGAAiG;AAQjG,uGAAuG;AACvG,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,OAAO;IACP,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,YAAY;IACZ,aAAa;IACb,eAAe;IACf,qBAAqB;CACtB,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,qBAAqB,GAAkC;IAClE,EAAE,OAAO,EAAE,gCAAgC,EAAE,OAAO,EAAE,4oEAA4oE,EAAE;IACpsE,EAAE,OAAO,EAAE,4CAA4C,EAAE,OAAO,EAAE,u/wBAAu/wB,EAAE;IAC3jxB,EAAE,OAAO,EAAE,6CAA6C,EAAE,OAAO,EAAE,kkQAAkkQ,EAAE;IACvoQ,EAAE,OAAO,EAAE,4CAA4C,EAAE,OAAO,EAAE,klqDAAklqD,EAAE;IACtpqD,EAAE,OAAO,EAAE,kCAAkC,EAAE,OAAO,EAAE,wGAAwG,EAAE;IAClK,EAAE,OAAO,EAAE,mCAAmC,EAAE,OAAO,EAAE,mfAAmf,EAAE;IAC9iB,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,yRAAyR,EAAE;IACtV,EAAE,OAAO,EAAE,oCAAoC,EAAE,OAAO,EAAE,i+LAAi+L,EAAE;IAC7hM,EAAE,OAAO,EAAE,8CAA8C,EAAE,OAAO,EAAE,okLAAokL,EAAE;IAC1oL,EAAE,OAAO,EAAE,iDAAiD,EAAE,OAAO,EAAE,8zHAA8zH,EAAE;IACv4H,EAAE,OAAO,EAAE,0CAA0C,EAAE,OAAO,EAAE,wyBAAwyB,EAAE;IAC12B,EAAE,OAAO,EAAE,uCAAuC,EAAE,OAAO,EAAE,uRAAuR,EAAE;IACtV,EAAE,OAAO,EAAE,mCAAmC,EAAE,OAAO,EAAE,2tIAA2tI,EAAE;IACtxI,EAAE,OAAO,EAAE,+CAA+C,EAAE,OAAO,EAAE,mrhCAAmrhC,EAAE;IAC1vhC,EAAE,OAAO,EAAE,+CAA+C,EAAE,OAAO,EAAE,g4sDAAg4sD,EAAE;IACv8sD,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,2GAA2G,EAAE;IACxK,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,sfAAsf,EAAE;IACpjB,EAAE,OAAO,EAAE,wCAAwC,EAAE,OAAO,EAAE,yRAAyR,EAAE;IACzV,EAAE,OAAO,EAAE,uCAAuC,EAAE,OAAO,EAAE,grOAAgrO,EAAE;IAC/uO,EAAE,OAAO,EAAE,0DAA0D,EAAE,OAAO,EAAE,4jLAA4jL,EAAE;IAC9oL,EAAE,OAAO,EAAE,oDAAoD,EAAE,OAAO,EAAE,ijHAAijH,EAAE;IAC7nH,EAAE,OAAO,EAAE,6CAA6C,EAAE,OAAO,EAAE,8/BAA8/B,EAAE;IACnkC,EAAE,OAAO,EAAE,0CAA0C,EAAE,OAAO,EAAE,uRAAuR,EAAE;IACzV,EAAE,OAAO,EAAE,+BAA+B,EAAE,OAAO,EAAE,w9LAAw9L,EAAE;IAC/gM,EAAE,OAAO,EAAE,2CAA2C,EAAE,OAAO,EAAE,o8nBAAo8nB,EAAE;IACvgoB,EAAE,OAAO,EAAE,2CAA2C,EAAE,OAAO,EAAE,k1nDAAk1nD,EAAE;IACr5nD,EAAE,OAAO,EAAE,iCAAiC,EAAE,OAAO,EAAE,uGAAuG,EAAE;IAChK,EAAE,OAAO,EAAE,kCAAkC,EAAE,OAAO,EAAE,kfAAkf,EAAE;IAC5iB,EAAE,OAAO,EAAE,oCAAoC,EAAE,OAAO,EAAE,qQAAqQ,EAAE;IACjU,EAAE,OAAO,EAAE,mCAAmC,EAAE,OAAO,EAAE,mqLAAmqL,EAAE;IAC9tL,EAAE,OAAO,EAAE,6CAA6C,EAAE,OAAO,EAAE,yrNAAyrN,EAAE;IAC9vN,EAAE,OAAO,EAAE,gDAAgD,EAAE,OAAO,EAAE,o6HAAo6H,EAAE;IAC5+H,EAAE,OAAO,EAAE,yCAAyC,EAAE,OAAO,EAAE,y0CAAy0C,EAAE;IAC14C,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,uRAAuR,EAAE;IACrV,EAAE,OAAO,EAAE,kCAAkC,EAAE,OAAO,EAAE,i/XAAi/X,EAAE;IAC3iY,EAAE,OAAO,EAAE,oCAAoC,EAAE,OAAO,EAAE,6EAA6E,EAAE;IACzI,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,mXAAmX,EAAE;IAChb,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,48JAA48J,EAAE;IAC1gK,EAAE,OAAO,EAAE,4CAA4C,EAAE,OAAO,EAAE,uyCAAuyC,EAAE;IAC32C,EAAE,OAAO,EAAE,kCAAkC,EAAE,OAAO,EAAE,26KAA26K,EAAE;IACr+K,EAAE,OAAO,EAAE,oCAAoC,EAAE,OAAO,EAAE,0GAA0G,EAAE;IACtK,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,mlBAAmlB,EAAE;IAChpB,EAAE,OAAO,EAAE,uCAAuC,EAAE,OAAO,EAAE,gqBAAgqB,EAAE;IAC/tB,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,22cAA22c,EAAE;IACz6c,EAAE,OAAO,EAAE,wDAAwD,EAAE,OAAO,EAAE,yzJAAyzJ,EAAE;IACz4J,EAAE,OAAO,EAAE,oDAAoD,EAAE,OAAO,EAAE,qytBAAqytB,EAAE;IACj3tB,EAAE,OAAO,EAAE,mDAAmD,EAAE,OAAO,EAAE,ynYAAynY,EAAE;IACpsY,EAAE,OAAO,EAAE,4CAA4C,EAAE,OAAO,EAAE,osHAAosH,EAAE;IACxwH,EAAE,OAAO,EAAE,yCAAyC,EAAE,OAAO,EAAE,6iCAA6iC,EAAE;IAC9mC,EAAE,OAAO,EAAE,wCAAwC,EAAE,OAAO,EAAE,4sFAA4sF,EAAE;IAC5wF,EAAE,OAAO,EAAE,0CAA0C,EAAE,OAAO,EAAE,mFAAmF,EAAE;IACrJ,EAAE,OAAO,EAAE,2CAA2C,EAAE,OAAO,EAAE,yXAAyX,EAAE;IAC5b,EAAE,OAAO,EAAE,4CAA4C,EAAE,OAAO,EAAE,ollBAAollB,EAAE;IACxplB,EAAE,OAAO,EAAE,kDAAkD,EAAE,OAAO,EAAE,sgVAAsgV,EAAE;IAChlV,EAAE,OAAO,EAAE,+CAA+C,EAAE,OAAO,EAAE,oiCAAoiC,EAAE;IAC3mC,EAAE,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,2tCAA2tC,EAAE;IAC7wC,EAAE,OAAO,EAAE,4BAA4B,EAAE,OAAO,EAAE,iGAAiG,EAAE;IACrJ,EAAE,OAAO,EAAE,6BAA6B,EAAE,OAAO,EAAE,2WAA2W,EAAE;IACha,EAAE,OAAO,EAAE,8BAA8B,EAAE,OAAO,EAAE,s1BAAs1B,EAAE;IAC54B,EAAE,OAAO,EAAE,oCAAoC,EAAE,OAAO,EAAE,msBAAmsB,EAAE;IAC/vB,EAAE,OAAO,EAAE,4BAA4B,EAAE,OAAO,EAAE,q3JAAq3J,EAAE;IACz6J,EAAE,OAAO,EAAE,8BAA8B,EAAE,OAAO,EAAE,uEAAuE,EAAE;IAC7H,EAAE,OAAO,EAAE,+BAA+B,EAAE,OAAO,EAAE,6WAA6W,EAAE;IACpa,EAAE,OAAO,EAAE,gCAAgC,EAAE,OAAO,EAAE,kmTAAkmT,EAAE;IAC1pT,EAAE,OAAO,EAAE,sCAAsC,EAAE,OAAO,EAAE,gzCAAgzC,EAAE;CAC/2C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../src/generated/surface.ts"],"names":[],"mappings":"AAIA,qGAAqG;AACrG,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EA2DzC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../src/generated/surface.ts"],"names":[],"mappings":"AAIA,qGAAqG;AACrG,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EA2DzC,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAsBxC,CAAC;AAEF,iGAAiG;AACjG,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAqDhD,CAAC;AAEF,oGAAoG;AACpG,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAaxC,CAAC"}
|
|
@@ -66,10 +66,16 @@ export const CLI_COMMANDS = [
|
|
|
66
66
|
export const SDK_EXPORTS = [
|
|
67
67
|
'annotations',
|
|
68
68
|
'asset',
|
|
69
|
+
'bind',
|
|
70
|
+
'clientCredentials',
|
|
71
|
+
'connection',
|
|
69
72
|
'connector',
|
|
70
73
|
'customerAuth',
|
|
71
74
|
'embeddedAssistant',
|
|
75
|
+
'externalExchange',
|
|
76
|
+
'gmailConnector',
|
|
72
77
|
'handoffSession',
|
|
78
|
+
'managedSecret',
|
|
73
79
|
'openAICompatible',
|
|
74
80
|
'prompt',
|
|
75
81
|
'resource',
|
|
@@ -128,6 +134,10 @@ export const COMPILE_ERROR_CODES = [
|
|
|
128
134
|
'unknown_connector_alias',
|
|
129
135
|
'connector_not_in_catalog',
|
|
130
136
|
'unknown_operation',
|
|
137
|
+
'connector_binding_required',
|
|
138
|
+
'unsupported_credential_profile',
|
|
139
|
+
'credential_scope_mismatch',
|
|
140
|
+
'credential_audience_mismatch',
|
|
131
141
|
'unused_connector_alias',
|
|
132
142
|
'arg_mismatch',
|
|
133
143
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.js","sourceRoot":"","sources":["../../src/generated/surface.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,6EAA6E;AAC7E,iGAAiG;AAEjG,qGAAqG;AACrG,MAAM,CAAC,MAAM,YAAY,GAAsB;IAC7C,MAAM;IACN,SAAS;IACT,UAAU;IACV,UAAU;IACV,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,OAAO;IACP,MAAM;IACN,OAAO;IACP,WAAW;IACX,SAAS;IACT,KAAK;IACL,UAAU;IACV,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,QAAQ;IACR,eAAe;IACf,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,SAAS;IACT,OAAO;IACP,UAAU;IACV,SAAS;IACT,SAAS;IACT,QAAQ;IACR,MAAM;IACN,MAAM;IACN,aAAa;IACb,SAAS;IACT,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,SAAS;IACT,WAAW;CACZ,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAsB;IAC5C,aAAa;IACb,OAAO;IACP,WAAW;IACX,cAAc;IACd,mBAAmB;IACnB,gBAAgB;IAChB,kBAAkB;IAClB,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,UAAU;IACV,MAAM;IACN,GAAG;CACJ,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,0BAA0B;IAC1B,kBAAkB;IAClB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,eAAe;IACf,8BAA8B;IAC9B,6BAA6B;IAC7B,uBAAuB;IACvB,cAAc;IACd,oBAAoB;IACpB,oBAAoB;IACpB,qBAAqB;IACrB,oBAAoB;IACpB,mBAAmB;IACnB,uBAAuB;IACvB,2BAA2B;IAC3B,qBAAqB;IACrB,kBAAkB;IAClB,kBAAkB;IAClB,eAAe;IACf,mBAAmB;IACnB,oBAAoB;IACpB,4BAA4B;IAC5B,0BAA0B;IAC1B,2BAA2B;IAC3B,mBAAmB;IACnB,wBAAwB;IACxB,oBAAoB;IACpB,kBAAkB;IAClB,wBAAwB;IACxB,0BAA0B;IAC1B,kBAAkB;IAClB,qBAAqB;IACrB,4BAA4B;IAC5B,uBAAuB;IACvB,wBAAwB;IACxB,6BAA6B;IAC7B,uBAAuB;IACvB,6BAA6B;IAC7B,eAAe;IACf,gCAAgC;IAChC,oBAAoB;IACpB,yBAAyB;IACzB,0BAA0B;IAC1B,mBAAmB;IACnB,wBAAwB;IACxB,cAAc;CACf,CAAC;AAEF,oGAAoG;AACpG,MAAM,CAAC,MAAM,WAAW,GAAsB;IAC5C,aAAa;IACb,aAAa;IACb,cAAc;IACd,WAAW;IACX,aAAa;IACb,uBAAuB;IACvB,iBAAiB;IACjB,wBAAwB;IACxB,uBAAuB;IACvB,oBAAoB;IACpB,YAAY;IACZ,YAAY;CACb,CAAC"}
|
|
1
|
+
{"version":3,"file":"surface.js","sourceRoot":"","sources":["../../src/generated/surface.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,6EAA6E;AAC7E,iGAAiG;AAEjG,qGAAqG;AACrG,MAAM,CAAC,MAAM,YAAY,GAAsB;IAC7C,MAAM;IACN,SAAS;IACT,UAAU;IACV,UAAU;IACV,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,OAAO;IACP,MAAM;IACN,OAAO;IACP,WAAW;IACX,SAAS;IACT,KAAK;IACL,UAAU;IACV,OAAO;IACP,SAAS;IACT,MAAM;IACN,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,QAAQ;IACR,eAAe;IACf,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,SAAS;IACT,OAAO;IACP,UAAU;IACV,SAAS;IACT,SAAS;IACT,QAAQ;IACR,MAAM;IACN,MAAM;IACN,aAAa;IACb,SAAS;IACT,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,SAAS;IACT,WAAW;CACZ,CAAC;AAEF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAsB;IAC5C,aAAa;IACb,OAAO;IACP,MAAM;IACN,mBAAmB;IACnB,YAAY;IACZ,WAAW;IACX,cAAc;IACd,mBAAmB;IACnB,kBAAkB;IAClB,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,kBAAkB;IAClB,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,UAAU;IACV,MAAM;IACN,GAAG;CACJ,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,0BAA0B;IAC1B,kBAAkB;IAClB,eAAe;IACf,cAAc;IACd,gBAAgB;IAChB,eAAe;IACf,8BAA8B;IAC9B,6BAA6B;IAC7B,uBAAuB;IACvB,cAAc;IACd,oBAAoB;IACpB,oBAAoB;IACpB,qBAAqB;IACrB,oBAAoB;IACpB,mBAAmB;IACnB,uBAAuB;IACvB,2BAA2B;IAC3B,qBAAqB;IACrB,kBAAkB;IAClB,kBAAkB;IAClB,eAAe;IACf,mBAAmB;IACnB,oBAAoB;IACpB,4BAA4B;IAC5B,0BAA0B;IAC1B,2BAA2B;IAC3B,mBAAmB;IACnB,wBAAwB;IACxB,oBAAoB;IACpB,kBAAkB;IAClB,wBAAwB;IACxB,0BAA0B;IAC1B,kBAAkB;IAClB,qBAAqB;IACrB,4BAA4B;IAC5B,uBAAuB;IACvB,wBAAwB;IACxB,6BAA6B;IAC7B,uBAAuB;IACvB,6BAA6B;IAC7B,eAAe;IACf,gCAAgC;IAChC,oBAAoB;IACpB,yBAAyB;IACzB,0BAA0B;IAC1B,mBAAmB;IACnB,4BAA4B;IAC5B,gCAAgC;IAChC,2BAA2B;IAC3B,8BAA8B;IAC9B,wBAAwB;IACxB,cAAc;CACf,CAAC;AAEF,oGAAoG;AACpG,MAAM,CAAC,MAAM,WAAW,GAAsB;IAC5C,aAAa;IACb,aAAa;IACb,cAAc;IACd,WAAW;IACX,aAAa;IACb,uBAAuB;IACvB,iBAAiB;IACjB,wBAAwB;IACxB,uBAAuB;IACvB,oBAAoB;IACpB,YAAY;IACZ,YAAY;CACb,CAAC"}
|