@noodleseed/one 0.155.0 → 0.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/assistant-embed-django-tests.d.ts +3 -0
- package/dist/assistant-embed-django-tests.d.ts.map +1 -0
- package/dist/assistant-embed-django-tests.js +140 -0
- package/dist/assistant-embed-django-tests.js.map +1 -0
- package/dist/assistant-embed-django.d.ts +4 -0
- package/dist/assistant-embed-django.d.ts.map +1 -0
- package/dist/assistant-embed-django.js +259 -0
- package/dist/assistant-embed-django.js.map +1 -0
- package/dist/assistant-embed-installation.d.ts +29 -0
- package/dist/assistant-embed-installation.d.ts.map +1 -0
- package/dist/assistant-embed-installation.js +124 -0
- package/dist/assistant-embed-installation.js.map +1 -0
- package/dist/assistant-embed-profile.d.ts +8 -0
- package/dist/assistant-embed-profile.d.ts.map +1 -0
- package/dist/assistant-embed-profile.js +150 -0
- package/dist/assistant-embed-profile.js.map +1 -0
- package/dist/assistant-embed-scaffold-template.d.ts +3 -2
- package/dist/assistant-embed-scaffold-template.d.ts.map +1 -1
- package/dist/assistant-embed-scaffold-template.js +39 -84
- package/dist/assistant-embed-scaffold-template.js.map +1 -1
- package/dist/assistant-embed-tests.d.ts +5 -0
- package/dist/assistant-embed-tests.d.ts.map +1 -0
- package/dist/assistant-embed-tests.js +106 -0
- package/dist/assistant-embed-tests.js.map +1 -0
- package/dist/assistant-embed-vue.d.ts +3 -0
- package/dist/assistant-embed-vue.d.ts.map +1 -0
- package/dist/assistant-embed-vue.js +77 -0
- package/dist/assistant-embed-vue.js.map +1 -0
- package/dist/commands/assistant-embed-ops.d.ts.map +1 -1
- package/dist/commands/assistant-embed-ops.js +112 -63
- package/dist/commands/assistant-embed-ops.js.map +1 -1
- package/dist/commands/author-loop.d.ts.map +1 -1
- package/dist/commands/author-loop.js +24 -28
- package/dist/commands/author-loop.js.map +1 -1
- package/dist/commands/deploy-first-flow.d.ts.map +1 -1
- package/dist/commands/deploy-first-flow.js +26 -15
- package/dist/commands/deploy-first-flow.js.map +1 -1
- package/dist/commands/deploy-preflight.d.ts +5 -0
- package/dist/commands/deploy-preflight.d.ts.map +1 -1
- package/dist/commands/deploy-preflight.js +44 -5
- package/dist/commands/deploy-preflight.js.map +1 -1
- package/dist/commands/local-readiness.d.ts +9 -1
- package/dist/commands/local-readiness.d.ts.map +1 -1
- package/dist/commands/local-readiness.js +74 -4
- package/dist/commands/local-readiness.js.map +1 -1
- package/dist/commands/project-setup.d.ts.map +1 -1
- package/dist/commands/project-setup.js +10 -15
- package/dist/commands/project-setup.js.map +1 -1
- package/dist/control-plane-request.d.ts +4 -0
- package/dist/control-plane-request.d.ts.map +1 -1
- package/dist/control-plane-request.js +40 -20
- package/dist/control-plane-request.js.map +1 -1
- package/dist/project-scaffold-model.d.ts +14 -0
- package/dist/project-scaffold-model.d.ts.map +1 -0
- package/dist/project-scaffold-model.js +36 -0
- package/dist/project-scaffold-model.js.map +1 -0
- package/dist/project-scaffold-templates.d.ts.map +1 -1
- package/dist/project-scaffold-templates.js +73 -39
- package/dist/project-scaffold-templates.js.map +1 -1
- package/dist/project-scaffold-test-template.d.ts +4 -0
- package/dist/project-scaffold-test-template.d.ts.map +1 -0
- package/dist/project-scaffold-test-template.js +89 -0
- package/dist/project-scaffold-test-template.js.map +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +1 -2
- package/dist/project.js.map +1 -1
- package/dist/widget-focused-template.d.ts +1 -1
- package/dist/widget-focused-template.d.ts.map +1 -1
- package/dist/widget-focused-template.js +34 -12
- package/dist/widget-focused-template.js.map +1 -1
- package/dist/widget-scaffold-template.d.ts +1 -1
- package/dist/widget-scaffold-template.d.ts.map +1 -1
- package/dist/widget-scaffold-template.js +75 -43
- package/dist/widget-scaffold-template.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +17 -3
- package/node_modules/@noodle-borg/agent-kit/dist/skill-app-playbook-ref.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +9 -12
- package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-operations-ref.js +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +32 -26
- package/node_modules/@noodle-borg/agent-kit/dist/skill-server-playbook-ref.js +2 -2
- package/node_modules/@noodle-borg/agent-kit/dist/skill-verification-ref.js +2 -1
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +6 -0
- package/node_modules/@noodle-borg/authoring/dist/recording.d.ts +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +10 -4
- package/node_modules/@noodle-borg/compiler/package.json +0 -1
- package/node_modules/@noodle-borg/deploy-client/dist/deploy-payload-size.js +6 -2
- package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +193 -96
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +3 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/index.js +3 -0
- package/node_modules/@noodleseed/assistant/README.md +21 -23
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +5 -4
- package/dist/saas-scaffold-template.d.ts +0 -4
- package/dist/saas-scaffold-template.d.ts.map +0 -1
- package/dist/saas-scaffold-template.js +0 -56
- package/dist/saas-scaffold-template.js.map +0 -1
- package/node_modules/tailwindcss/LICENSE +0 -21
- package/node_modules/tailwindcss/README.md +0 -36
- package/node_modules/tailwindcss/dist/chunk-5JIJA4QV.mjs +0 -1
- package/node_modules/tailwindcss/dist/chunk-HMCCH6MG.mjs +0 -1
- package/node_modules/tailwindcss/dist/chunk-X4GG3EDV.mjs +0 -1
- package/node_modules/tailwindcss/dist/colors-C__qRT83.d.ts +0 -347
- package/node_modules/tailwindcss/dist/colors.d.mts +0 -347
- package/node_modules/tailwindcss/dist/colors.d.ts +0 -5
- package/node_modules/tailwindcss/dist/colors.js +0 -1
- package/node_modules/tailwindcss/dist/colors.mjs +0 -1
- package/node_modules/tailwindcss/dist/default-theme.d.mts +0 -1199
- package/node_modules/tailwindcss/dist/default-theme.d.ts +0 -1199
- package/node_modules/tailwindcss/dist/default-theme.js +0 -1
- package/node_modules/tailwindcss/dist/default-theme.mjs +0 -1
- package/node_modules/tailwindcss/dist/flatten-color-palette.d.mts +0 -6
- package/node_modules/tailwindcss/dist/flatten-color-palette.d.ts +0 -6
- package/node_modules/tailwindcss/dist/flatten-color-palette.js +0 -1
- package/node_modules/tailwindcss/dist/flatten-color-palette.mjs +0 -1
- package/node_modules/tailwindcss/dist/lib.d.mts +0 -379
- package/node_modules/tailwindcss/dist/lib.d.ts +0 -3
- package/node_modules/tailwindcss/dist/lib.js +0 -38
- package/node_modules/tailwindcss/dist/lib.mjs +0 -38
- package/node_modules/tailwindcss/dist/plugin.d.mts +0 -11
- package/node_modules/tailwindcss/dist/plugin.d.ts +0 -134
- package/node_modules/tailwindcss/dist/plugin.js +0 -1
- package/node_modules/tailwindcss/dist/plugin.mjs +0 -1
- package/node_modules/tailwindcss/dist/resolve-config-B4yBzhca.d.ts +0 -29
- package/node_modules/tailwindcss/dist/resolve-config-QUZ9b-Gn.d.mts +0 -190
- package/node_modules/tailwindcss/dist/types-DWdTiksJ.d.mts +0 -128
- package/node_modules/tailwindcss/index.css +0 -944
- package/node_modules/tailwindcss/package.json +0 -89
- package/node_modules/tailwindcss/preflight.css +0 -393
- package/node_modules/tailwindcss/theme.css +0 -510
- package/node_modules/tailwindcss/utilities.css +0 -1
package/README.md
CHANGED
|
@@ -58,13 +58,17 @@ Create and run a local project without a Noodle Seed account:
|
|
|
58
58
|
npx @noodleseed/one@latest init my-noodle-app
|
|
59
59
|
cd my-noodle-app
|
|
60
60
|
npm install
|
|
61
|
+
npm test
|
|
61
62
|
npm run validate
|
|
62
|
-
npm run dev
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
The generated project contains a normal TypeScript entrypoint, tests, npm scripts, `noodle.json`, and
|
|
66
66
|
project-local instructions for supported coding agents. It remains your project: edit it with your existing
|
|
67
67
|
tools, commit it to your repository, and run it locally before deciding to deploy.
|
|
68
|
+
The default SaaS starter is embedded-first and explicitly synthetic: its tests check a useful local result
|
|
69
|
+
and invalid-input rejection without customer credentials or live backends. Bind the names in `.env.example`
|
|
70
|
+
before `npm run dev`; wire the generated integration seams to your existing application before claiming
|
|
71
|
+
customer authentication or saved business changes. `--template hello` needs no configuration.
|
|
68
72
|
|
|
69
73
|
For frequent CLI use, you can install the same package globally:
|
|
70
74
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-django-tests.d.ts","sourceRoot":"","sources":["../src/assistant-embed-django-tests.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,wBAAgB,mBAAmB,IAAI,MAAM,CAyI5C"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Tests ship with the Python profile and run through the customer's existing Django test runner. */
|
|
2
|
+
export function djangoContractTests() {
|
|
3
|
+
return `import json
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from django.test import Client, SimpleTestCase, override_settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@override_settings(
|
|
11
|
+
ROOT_URLCONF="noodle_assistant.urls", ALLOWED_HOSTS=["app.example"],
|
|
12
|
+
MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
|
|
13
|
+
PUBLIC_APP_ORIGIN="https://app.example", NOODLE_SERVICE_URL="https://cloud.example",
|
|
14
|
+
NOODLE_ASSISTANT_CLIENT_ID="synthetic-client", NOODLE_ASSISTANT_CLIENT_SECRET="synthetic-test-credential",
|
|
15
|
+
)
|
|
16
|
+
class AssistantSessionContractTests(SimpleTestCase):
|
|
17
|
+
"""Synthetic route proof. Add real session/membership fixtures in the application's own suite."""
|
|
18
|
+
|
|
19
|
+
def setUp(self):
|
|
20
|
+
self.client = Client(enforce_csrf_checks=True)
|
|
21
|
+
self.csrf = "a" * 32
|
|
22
|
+
self.client.cookies["csrftoken"] = self.csrf
|
|
23
|
+
self.identity_patch = patch("noodle_assistant.views.authenticate_assistant_request", return_value=None)
|
|
24
|
+
self.authenticate = self.identity_patch.start()
|
|
25
|
+
self.addCleanup(self.identity_patch.stop)
|
|
26
|
+
self.exchange_patch = patch("noodle_assistant.views.requests.post")
|
|
27
|
+
self.exchange = self.exchange_patch.start()
|
|
28
|
+
self.addCleanup(self.exchange_patch.stop)
|
|
29
|
+
self.session = {
|
|
30
|
+
"token": "synthetic-session", "expiresAt": "2030-01-01T00:00:00Z",
|
|
31
|
+
"endpoints": {"turns": "https://cloud.example/turns", "toolConfirmations": "https://cloud.example/confirmations"},
|
|
32
|
+
}
|
|
33
|
+
upstream = Mock(status_code=201)
|
|
34
|
+
upstream.iter_content.return_value = [json.dumps(self.session).encode()]
|
|
35
|
+
self.upstream = upstream
|
|
36
|
+
self.exchange.return_value.__enter__.return_value = upstream
|
|
37
|
+
|
|
38
|
+
def post(self, body=None, **headers):
|
|
39
|
+
defaults = {"HTTP_HOST": "app.example", "HTTP_ORIGIN": "https://app.example", "HTTP_X_CSRFTOKEN": self.csrf}
|
|
40
|
+
defaults.update(headers)
|
|
41
|
+
return self.client.post("/api/assistant/session", json.dumps({} if body is None else body), content_type="application/json", secure=True, **defaults)
|
|
42
|
+
|
|
43
|
+
def signed_in(self):
|
|
44
|
+
self.authenticate.return_value = {
|
|
45
|
+
"user": {"id": "person-a", "tenant": "tenant-a", "scopes": ["tasks:read"]},
|
|
46
|
+
"routing": {"endpoints": {"customer_api": "https://tenant-a.example/api"}},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def test_signed_out_is_json_401_not_redirect(self):
|
|
50
|
+
response = self.post()
|
|
51
|
+
self.assertEqual(response.status_code, 401)
|
|
52
|
+
self.assertEqual(response.json()["code"], "authentication_required")
|
|
53
|
+
self.assertNotIn("Location", response)
|
|
54
|
+
self.assertEqual(response["Cache-Control"], "no-store")
|
|
55
|
+
self.exchange.assert_not_called()
|
|
56
|
+
|
|
57
|
+
def test_csrf_is_not_exempted(self):
|
|
58
|
+
self.assertEqual(self.post(HTTP_X_CSRFTOKEN="").status_code, 403)
|
|
59
|
+
self.authenticate.assert_not_called()
|
|
60
|
+
self.exchange.assert_not_called()
|
|
61
|
+
|
|
62
|
+
def test_wrong_origin_is_refused_before_identity_or_exchange(self):
|
|
63
|
+
self.assertEqual(self.post(HTTP_ORIGIN="https://attacker.example").status_code, 403)
|
|
64
|
+
self.authenticate.assert_not_called()
|
|
65
|
+
self.exchange.assert_not_called()
|
|
66
|
+
|
|
67
|
+
def test_form_content_type_is_refused(self):
|
|
68
|
+
response = self.client.post("/api/assistant/session", "{}", content_type="text/plain", secure=True,
|
|
69
|
+
HTTP_HOST="app.example", HTTP_ORIGIN="https://app.example", HTTP_X_CSRFTOKEN=self.csrf)
|
|
70
|
+
self.assertEqual(response.status_code, 415)
|
|
71
|
+
self.exchange.assert_not_called()
|
|
72
|
+
|
|
73
|
+
def test_browser_cannot_choose_identity_or_routes(self):
|
|
74
|
+
self.signed_in()
|
|
75
|
+
for body in ({"user": {"id": "person-b"}}, {"routing": {}}, {"claims": {"admin": True}},
|
|
76
|
+
{"context": {"nested": {}}}, {"context": {"number": float("inf")}},
|
|
77
|
+
{"context": {"number": 10 ** 1_000}}, {"context": {"text": "x" * 2_001}}):
|
|
78
|
+
with self.subTest(body_kind=next(iter(body))):
|
|
79
|
+
self.assertEqual(self.post(body).status_code, 400)
|
|
80
|
+
self.exchange.assert_not_called()
|
|
81
|
+
|
|
82
|
+
def test_body_limit(self):
|
|
83
|
+
self.signed_in()
|
|
84
|
+
self.assertEqual(self.post({"context": {"page": "x" * 16_384}}).status_code, 413)
|
|
85
|
+
self.exchange.assert_not_called()
|
|
86
|
+
|
|
87
|
+
def test_deep_json_is_refused_without_a_server_error(self):
|
|
88
|
+
self.signed_in()
|
|
89
|
+
nested = "[" * 3_000 + "0" + "]" * 3_000
|
|
90
|
+
response = self.client.post("/api/assistant/session", nested, content_type="application/json", secure=True,
|
|
91
|
+
HTTP_HOST="app.example", HTTP_ORIGIN="https://app.example", HTTP_X_CSRFTOKEN=self.csrf)
|
|
92
|
+
self.assertEqual(response.status_code, 400)
|
|
93
|
+
self.exchange.assert_not_called()
|
|
94
|
+
self.upstream.iter_content.return_value = [nested.encode()]
|
|
95
|
+
self.assertEqual(self.post().status_code, 502)
|
|
96
|
+
# Older supported Python parsers can exhaust recursion before reaching the byte limit.
|
|
97
|
+
with patch("noodle_assistant.views.json.loads", side_effect=RecursionError):
|
|
98
|
+
self.assertEqual(self.post().status_code, 400)
|
|
99
|
+
with patch("noodle_assistant.views.json.loads", side_effect=[{}, RecursionError()]):
|
|
100
|
+
self.assertEqual(self.post().status_code, 502)
|
|
101
|
+
|
|
102
|
+
def test_success_uses_backend_identity_without_forwarding_cookies(self):
|
|
103
|
+
self.signed_in()
|
|
104
|
+
response = self.post({"context": {"page": "tasks"}}, HTTP_AUTHORIZATION="Bearer synthetic-inbound")
|
|
105
|
+
self.assertEqual(response.status_code, 200)
|
|
106
|
+
self.assertEqual(response.json(), self.session)
|
|
107
|
+
self.assertEqual(response["Cache-Control"], "no-store")
|
|
108
|
+
sent = self.exchange.call_args.kwargs
|
|
109
|
+
self.assertEqual(sent["json"]["user"]["tenant"], "tenant-a")
|
|
110
|
+
self.assertEqual(sent["json"]["routing"], self.authenticate.return_value["routing"])
|
|
111
|
+
self.assertFalse(sent["allow_redirects"])
|
|
112
|
+
self.assertEqual(sent["timeout"], (3, 10))
|
|
113
|
+
self.assertNotIn("synthetic-inbound", str(sent))
|
|
114
|
+
self.assertNotIn("Cookie", sent["headers"])
|
|
115
|
+
|
|
116
|
+
def test_exchange_failure_is_sanitized(self):
|
|
117
|
+
self.signed_in()
|
|
118
|
+
self.exchange.side_effect = requests.Timeout("private upstream details")
|
|
119
|
+
response = self.post()
|
|
120
|
+
self.assertEqual(response.status_code, 502)
|
|
121
|
+
self.assertNotIn(b"private", response.content)
|
|
122
|
+
self.exchange.assert_called_once()
|
|
123
|
+
|
|
124
|
+
def test_redirect_and_html_are_not_forwarded(self):
|
|
125
|
+
self.signed_in()
|
|
126
|
+
self.upstream.status_code = 302
|
|
127
|
+
self.assertEqual(self.post().status_code, 502)
|
|
128
|
+
self.upstream.status_code = 200
|
|
129
|
+
self.upstream.iter_content.return_value = [b"<html>private upstream details</html>"]
|
|
130
|
+
self.assertEqual(self.post().status_code, 502)
|
|
131
|
+
|
|
132
|
+
def test_missing_configuration_and_identity_failure_are_not_signed_out(self):
|
|
133
|
+
with override_settings(NOODLE_ASSISTANT_CLIENT_SECRET=""):
|
|
134
|
+
self.assertEqual(self.post().status_code, 503)
|
|
135
|
+
self.authenticate.side_effect = RuntimeError("private identity failure")
|
|
136
|
+
self.assertEqual(self.post().status_code, 503)
|
|
137
|
+
self.exchange.assert_not_called()
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=assistant-embed-django-tests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-django-tests.js","sourceRoot":"","sources":["../src/assistant-embed-django-tests.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuIR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Python infrastructure is delivered as reviewed source, not a new runtime package or identity bridge. */
|
|
2
|
+
export declare function djangoEmbedFiles(): Record<string, string>;
|
|
3
|
+
export declare function djangoIntegrationGuide(): string;
|
|
4
|
+
//# sourceMappingURL=assistant-embed-django.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-django.d.ts","sourceRoot":"","sources":["../src/assistant-embed-django.ts"],"names":[],"mappings":"AAGA,2GAA2G;AAC3G,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuMzD;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAsD/C"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { djangoContractTests } from './assistant-embed-django-tests.js';
|
|
2
|
+
import { vueEmbedFiles } from './assistant-embed-vue.js';
|
|
3
|
+
/** Python infrastructure is delivered as reviewed source, not a new runtime package or identity bridge. */
|
|
4
|
+
export function djangoEmbedFiles() {
|
|
5
|
+
return {
|
|
6
|
+
'noodle_assistant/__init__.py': '',
|
|
7
|
+
'noodle_assistant/auth.py': `from django.http import HttpRequest
|
|
8
|
+
|
|
9
|
+
from .contracts import AssistantIdentity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def authenticate_assistant_request(request: HttpRequest) -> AssistantIdentity | None:
|
|
13
|
+
"""The sole backend seam: use the existing session and server-owned membership lookup."""
|
|
14
|
+
# Return None when signed out. Derive user, tenant, roles, scopes and routing server-side.
|
|
15
|
+
# Never trust browser JSON, a raw cookie, or a user-selected URL as verified identity/routing.
|
|
16
|
+
# Do not redirect or log cookies/credentials. An unavailable identity service should raise.
|
|
17
|
+
return None
|
|
18
|
+
`,
|
|
19
|
+
'noodle_assistant/contracts.py': `from typing import TypedDict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequiredUser(TypedDict):
|
|
23
|
+
id: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AssistantUser(RequiredUser, total=False):
|
|
27
|
+
email: str
|
|
28
|
+
name: str
|
|
29
|
+
tenant: str
|
|
30
|
+
roles: list[str]
|
|
31
|
+
scopes: list[str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RequiredIdentity(TypedDict):
|
|
35
|
+
user: AssistantUser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AssistantIdentity(RequiredIdentity, total=False):
|
|
39
|
+
claims: dict[str, str | int | float | bool | None]
|
|
40
|
+
preferences: dict[str, str]
|
|
41
|
+
routing: dict[str, dict[str, str]]
|
|
42
|
+
`,
|
|
43
|
+
'noodle_assistant/views.py': `import json
|
|
44
|
+
import math
|
|
45
|
+
from urllib.parse import urlsplit
|
|
46
|
+
|
|
47
|
+
import requests
|
|
48
|
+
from django.conf import settings
|
|
49
|
+
from django.http import JsonResponse
|
|
50
|
+
from django.views.decorators.csrf import csrf_protect
|
|
51
|
+
|
|
52
|
+
from .auth import authenticate_assistant_request
|
|
53
|
+
|
|
54
|
+
MAX_BODY_BYTES = 16 * 1024
|
|
55
|
+
MAX_RESPONSE_BYTES = 256 * 1024
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def failure(status, code, message):
|
|
59
|
+
response = JsonResponse({"code": code, "error": message}, status=status)
|
|
60
|
+
response["Cache-Control"] = "no-store"
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def safe_origin(value):
|
|
65
|
+
if not isinstance(value, str) or any(ord(char) <= 32 or ord(char) == 127 for char in value):
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
url = urlsplit(value)
|
|
69
|
+
return (
|
|
70
|
+
bool(url.hostname) and "*" not in value and url.username is None
|
|
71
|
+
and url.password is None and url.port != 0
|
|
72
|
+
and value == f"{url.scheme}://{url.netloc}"
|
|
73
|
+
and (url.scheme == "https" or (
|
|
74
|
+
url.scheme == "http" and url.hostname in ("localhost", "127.0.0.1", "::1")
|
|
75
|
+
))
|
|
76
|
+
)
|
|
77
|
+
except ValueError:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def configuration():
|
|
82
|
+
names = ("PUBLIC_APP_ORIGIN", "NOODLE_SERVICE_URL", "NOODLE_ASSISTANT_CLIENT_ID", "NOODLE_ASSISTANT_CLIENT_SECRET")
|
|
83
|
+
values = [getattr(settings, name, None) for name in names]
|
|
84
|
+
if not all(isinstance(value, str) and value.strip() for value in values):
|
|
85
|
+
return None
|
|
86
|
+
origin, service, client_id, client_secret = values
|
|
87
|
+
service = service.rstrip("/")
|
|
88
|
+
if not safe_origin(origin) or not safe_origin(service) or ":" in client_id:
|
|
89
|
+
return None
|
|
90
|
+
return origin, service, client_id, client_secret
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def finite_number(value):
|
|
94
|
+
try:
|
|
95
|
+
return isinstance(value, (int, float)) and math.isfinite(value)
|
|
96
|
+
except OverflowError:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def valid_context(value):
|
|
101
|
+
return isinstance(value, dict) and len(value) <= 32 and all(
|
|
102
|
+
isinstance(key, str) and len(key) <= 80 and (
|
|
103
|
+
item is None or isinstance(item, bool)
|
|
104
|
+
or finite_number(item)
|
|
105
|
+
or (isinstance(item, str) and len(item) <= 2_000)
|
|
106
|
+
) for key, item in value.items()
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@csrf_protect
|
|
111
|
+
def assistant_session(request):
|
|
112
|
+
"""Same-origin JSON endpoint. Keep Django's CsrfViewMiddleware enabled."""
|
|
113
|
+
if request.method != "POST":
|
|
114
|
+
response = failure(405, "method_not_allowed", "POST required")
|
|
115
|
+
response["Allow"] = "POST"
|
|
116
|
+
return response
|
|
117
|
+
config = configuration()
|
|
118
|
+
if config is None:
|
|
119
|
+
return failure(503, "assistant_not_configured", "assistant is not configured")
|
|
120
|
+
origin, service, client_id, client_secret = config
|
|
121
|
+
if request.headers.get("Origin") != origin:
|
|
122
|
+
return failure(403, "origin_not_allowed", "origin is not allowed")
|
|
123
|
+
if request.content_type != "application/json":
|
|
124
|
+
return failure(415, "unsupported_media_type", "application/json required")
|
|
125
|
+
try:
|
|
126
|
+
identity = authenticate_assistant_request(request)
|
|
127
|
+
except Exception:
|
|
128
|
+
return failure(503, "authentication_unavailable", "application authentication is unavailable")
|
|
129
|
+
if identity is None:
|
|
130
|
+
return failure(401, "authentication_required", "authentication required")
|
|
131
|
+
if not isinstance(identity, dict) or not isinstance(identity.get("user"), dict):
|
|
132
|
+
return failure(503, "authentication_unavailable", "application authentication is unavailable")
|
|
133
|
+
user_id = identity["user"].get("id")
|
|
134
|
+
if not isinstance(user_id, str) or not 1 <= len(user_id) <= 240:
|
|
135
|
+
return failure(503, "authentication_unavailable", "application authentication is unavailable")
|
|
136
|
+
try:
|
|
137
|
+
raw = request.read(MAX_BODY_BYTES + 1)
|
|
138
|
+
if len(raw) > MAX_BODY_BYTES:
|
|
139
|
+
return failure(413, "request_too_large", "request must not exceed 16 KiB")
|
|
140
|
+
body = json.loads(raw)
|
|
141
|
+
except (ValueError, OSError, RecursionError):
|
|
142
|
+
return failure(400, "invalid_request", "invalid JSON request")
|
|
143
|
+
if not isinstance(body, dict) or set(body) - {"context"}:
|
|
144
|
+
return failure(400, "invalid_request", "invalid request body")
|
|
145
|
+
if "context" in body and not valid_context(body["context"]):
|
|
146
|
+
return failure(400, "invalid_request", "invalid context")
|
|
147
|
+
payload = {"origin": origin, "user": identity["user"]}
|
|
148
|
+
for key in ("claims", "preferences", "routing"):
|
|
149
|
+
if key in identity:
|
|
150
|
+
payload[key] = identity[key]
|
|
151
|
+
if "context" in body:
|
|
152
|
+
payload["context"] = body["context"]
|
|
153
|
+
try:
|
|
154
|
+
# No redirects or retries; inbound cookies/bearers never reach the Noodle exchange.
|
|
155
|
+
with requests.post(
|
|
156
|
+
f"{service}/v1/assistant/sessions", auth=(client_id, client_secret),
|
|
157
|
+
headers={"Accept": "application/json"}, json=payload,
|
|
158
|
+
timeout=(3, 10), allow_redirects=False, stream=True,
|
|
159
|
+
) as upstream:
|
|
160
|
+
if upstream.status_code not in (200, 201):
|
|
161
|
+
return failure(502, "session_exchange_failed", "assistant session exchange failed")
|
|
162
|
+
data = bytearray()
|
|
163
|
+
for chunk in upstream.iter_content(chunk_size=16 * 1024):
|
|
164
|
+
data.extend(chunk)
|
|
165
|
+
if len(data) > MAX_RESPONSE_BYTES:
|
|
166
|
+
return failure(502, "session_exchange_failed", "assistant session exchange failed")
|
|
167
|
+
session = json.loads(data)
|
|
168
|
+
if not isinstance(session, dict) or not isinstance(session.get("token"), str) or not isinstance(session.get("endpoints"), dict):
|
|
169
|
+
return failure(502, "session_exchange_failed", "assistant session exchange failed")
|
|
170
|
+
except (requests.RequestException, ValueError, RecursionError):
|
|
171
|
+
return failure(502, "session_exchange_failed", "assistant session exchange failed")
|
|
172
|
+
response = JsonResponse(session)
|
|
173
|
+
response["Cache-Control"] = "no-store"
|
|
174
|
+
return response
|
|
175
|
+
`,
|
|
176
|
+
'noodle_assistant/urls.py': `from django.urls import path
|
|
177
|
+
|
|
178
|
+
from .views import assistant_session
|
|
179
|
+
|
|
180
|
+
urlpatterns = [path("api/assistant/session", assistant_session, name="noodle-assistant-session")]
|
|
181
|
+
`,
|
|
182
|
+
'noodle_assistant/tests.py': djangoContractTests(),
|
|
183
|
+
'noodle_assistant/settings.example.py': `# Merge these names into existing Django settings; use the application's secret/config loader.
|
|
184
|
+
# Never copy real credentials into source or expose them to the Vue build.
|
|
185
|
+
import os
|
|
186
|
+
|
|
187
|
+
NOODLE_SERVICE_URL = os.environ.get("NOODLE_SERVICE_URL")
|
|
188
|
+
NOODLE_ASSISTANT_CLIENT_ID = os.environ.get("NOODLE_ASSISTANT_CLIENT_ID")
|
|
189
|
+
NOODLE_ASSISTANT_CLIENT_SECRET = os.environ.get("NOODLE_ASSISTANT_CLIENT_SECRET")
|
|
190
|
+
PUBLIC_APP_ORIGIN = os.environ.get("PUBLIC_APP_ORIGIN")
|
|
191
|
+
# Keep django.middleware.csrf.CsrfViewMiddleware and existing authentication middleware enabled.
|
|
192
|
+
# Root URL configuration: path("", include("noodle_assistant.urls"))
|
|
193
|
+
`,
|
|
194
|
+
'.env.noodle-host.example': `# Backend settings only. Never copy these values into VITE_* or another browser-prefixed variable.
|
|
195
|
+
NOODLE_SERVICE_URL=https://cloud.noodleseed.dev
|
|
196
|
+
NOODLE_ASSISTANT_CLIENT_ID=
|
|
197
|
+
NOODLE_ASSISTANT_CLIENT_SECRET=
|
|
198
|
+
PUBLIC_APP_ORIGIN=http://localhost:5173
|
|
199
|
+
`,
|
|
200
|
+
...vueEmbedFiles(),
|
|
201
|
+
'NOODLE-INTEGRATION.md': djangoIntegrationGuide(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function djangoIntegrationGuide() {
|
|
205
|
+
return `# Django/Vue application integration
|
|
206
|
+
|
|
207
|
+
This profile is authenticated only. The existing Django application owns identity, membership and CSRF;
|
|
208
|
+
Vue stays a static frontend. Noodle authoring remains TypeScript in its own project. No Node production
|
|
209
|
+
server, new login system or direct database connector is introduced.
|
|
210
|
+
|
|
211
|
+
## Install and adapt
|
|
212
|
+
|
|
213
|
+
1. Use the existing Python environment and package manager to add requests. The profile is qualified on
|
|
214
|
+
Django 5.2 and 6.1; use a Python version supported by your Django release. It does not replace application
|
|
215
|
+
settings, middleware or URL configuration. Other versions require your own validation.
|
|
216
|
+
2. Include \`noodle_assistant.urls\` at the application root. Map the four names in
|
|
217
|
+
\`noodle_assistant/settings.example.py\` with your existing server-side config loader.
|
|
218
|
+
3. Implement only \`authenticate_assistant_request\` in \`noodle_assistant/auth.py\`: return None when signed
|
|
219
|
+
out, otherwise verified user/tenant/scopes, optional claims/preferences and membership-derived routing.
|
|
220
|
+
Keep actual business functions and authorization in the application; never trust browser identity fields.
|
|
221
|
+
4. Install a compatible \`@noodleseed/assistant\` using the Vue application's existing package manager.
|
|
222
|
+
Render \`NoodleAssistant.vue\` inside the signed-in application with a stable user/tenant \`principalKey\`
|
|
223
|
+
and the current \`csrfToken\` from your existing CSRF bootstrap. Change the key on account changes.
|
|
224
|
+
5. The wrapper sets the element's fetch property before mounting, adding \`X-CSRFToken\` only to the exact
|
|
225
|
+
same-origin session POST. It never sends that header to the Noodle service. Keep Django's normal CSRF
|
|
226
|
+
middleware. If your application uses a custom header name, adapt that one name with a regression test.
|
|
227
|
+
6. Route \`/api/assistant/session\` through the Vue page's public origin to Django. Do not enable cross-origin
|
|
228
|
+
cookies or widen trusted origins to make a split infrastructure deployment work.
|
|
229
|
+
|
|
230
|
+
The wrapper creates the Web Component directly, so Vue needs no custom-element compiler configuration.
|
|
231
|
+
For CSRF tokens stored in HttpOnly cookies or server sessions, reuse the application's existing masked-token
|
|
232
|
+
endpoint/DOM data. Do not expose an application session cookie or disable CSRF to make the test pass.
|
|
233
|
+
|
|
234
|
+
## Verify
|
|
235
|
+
|
|
236
|
+
- Run \`python manage.py test noodle_assistant\`: generated tests enforce CSRF and exercise the real view
|
|
237
|
+
using synthetic identity/exchange fixtures. A signed-out request with a valid CSRF token returns JSON 401;
|
|
238
|
+
an invalid/missing CSRF token is correctly refused by Django before the view.
|
|
239
|
+
- Run \`vitest run test/noodle-assistant-transport.test.ts\` using the existing frontend test setup (add
|
|
240
|
+
Vitest as a dev dependency if absent), then build the existing Vue application.
|
|
241
|
+
- Run \`noodle assistant embed --framework django-vue --check --json\` with host settings exported. It
|
|
242
|
+
reports static evidence only, not that Django imported settings or proxy rules are correct.
|
|
243
|
+
- Add actual signed-out, tenant-A, tenant-B and authorized identity fixtures to the existing test harness;
|
|
244
|
+
exercise one sandbox read and confirmed action. Verify the real browser's CSRF, CSP, streaming and App frame.
|
|
245
|
+
|
|
246
|
+
Keep the service in CSP connect-src and frame-src. Missing sandbox credentials, identities or permission
|
|
247
|
+
remain unverified. Generated tests do not prove the customer's session function or production readiness.
|
|
248
|
+
|
|
249
|
+
## Updates
|
|
250
|
+
|
|
251
|
+
Preview with \`noodle assistant embed --framework django-vue --dry-run --json\`. Reruns create missing files
|
|
252
|
+
and preserve modified code. Review conflicts; never use --force to erase an existing login implementation.
|
|
253
|
+
These files are customer-owned after installation. Keep settings, secret values and business logic during
|
|
254
|
+
upgrades and rerun the contract and application acceptance tests before deployment.
|
|
255
|
+
|
|
256
|
+
Guide: https://docs.noodleseed.dev/docs/guides/embedded-assistant-django-vue
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=assistant-embed-django.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-django.js","sourceRoot":"","sources":["../src/assistant-embed-django.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,2GAA2G;AAC3G,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,8BAA8B,EAAE,EAAE;QAClC,0BAA0B,EAAE;;;;;;;;;;;CAW/B;QACG,+BAA+B,EAAE;;;;;;;;;;;;;;;;;;;;;;;CAuBpC;QACG,2BAA2B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoIhC;QACG,0BAA0B,EAAE;;;;;CAK/B;QACG,2BAA2B,EAAE,mBAAmB,EAAE;QAClD,sCAAsC,EAAE;;;;;;;;;;CAU3C;QACG,0BAA0B,EAAE;;;;;CAK/B;QACG,GAAG,aAAa,EAAE;QAClB,uBAAuB,EAAE,sBAAsB,EAAE;KAClD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EmbedFramework, EmbedSurface } from './assistant-embed-scaffold-template.js';
|
|
2
|
+
export interface EmbedFileAction {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly action: 'created' | 'unchanged' | 'skipped' | 'overwritten';
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
readonly content?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Inspect every fixed output before writing any of them; do not follow customer symlinks. */
|
|
9
|
+
export declare function prepareEmbedInstallation(dir: string, contents: Readonly<Record<string, string>>, options: {
|
|
10
|
+
readonly force: boolean;
|
|
11
|
+
readonly dryRun: boolean;
|
|
12
|
+
}): readonly EmbedFileAction[];
|
|
13
|
+
/** Include instruction outputs in the same pre-write boundary as generated application files. */
|
|
14
|
+
export declare function assertEmbedOutputPaths(dir: string, paths: readonly string[]): void;
|
|
15
|
+
export declare function applyEmbedInstallation(dir: string, contents: Readonly<Record<string, string>>, files: readonly EmbedFileAction[]): void;
|
|
16
|
+
/** Tool-facing recipe metadata describes installed assets, never a parallel authoring language. */
|
|
17
|
+
export declare function embedRecipe(framework: EmbedFramework, surface: EmbedSurface, files: readonly EmbedFileAction[]): {
|
|
18
|
+
id: string;
|
|
19
|
+
version: number;
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
appliesTo: string[];
|
|
22
|
+
doesNotApplyWhen: string[];
|
|
23
|
+
dependencies: string[];
|
|
24
|
+
applicationSeams: string[];
|
|
25
|
+
ownership: string;
|
|
26
|
+
verification: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare function embedNextSteps(surface: EmbedSurface, framework?: EmbedFramework): readonly string[];
|
|
29
|
+
//# sourceMappingURL=assistant-embed-installation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-installation.d.ts","sourceRoot":"","sources":["../src/assistant-embed-installation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAE3F,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,CAAC;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAMD,8FAA8F;AAC9F,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC1C,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC7D,SAAS,eAAe,EAAE,CAe5B;AAED,iGAAiG;AACjG,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAUlF;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC1C,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,IAAI,CAiBN;AAED,mGAAmG;AACnG,wBAAgB,WAAW,CACzB,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,SAAS,eAAe,EAAE;;;;;;;;;;EAwClC;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,SAAS,GAAE,cAAyB,GACnC,SAAS,MAAM,EAAE,CAiCnB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { embedCheckCommand } from './assistant-embed-profile.js';
|
|
5
|
+
function hash(content) {
|
|
6
|
+
return createHash('sha256').update(content).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
/** Inspect every fixed output before writing any of them; do not follow customer symlinks. */
|
|
9
|
+
export function prepareEmbedInstallation(dir, contents, options) {
|
|
10
|
+
assertEmbedOutputPaths(dir, Object.keys(contents));
|
|
11
|
+
return Object.entries(contents).map(([path, content]) => {
|
|
12
|
+
const full = join(dir, path);
|
|
13
|
+
const existing = existsSync(full) ? readFileSync(full, 'utf8') : undefined;
|
|
14
|
+
const action = existing === content
|
|
15
|
+
? 'unchanged'
|
|
16
|
+
: existing === undefined
|
|
17
|
+
? 'created'
|
|
18
|
+
: options.force
|
|
19
|
+
? 'overwritten'
|
|
20
|
+
: 'skipped';
|
|
21
|
+
return { path, action, sha256: hash(content), ...(options.dryRun ? { content } : {}) };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Include instruction outputs in the same pre-write boundary as generated application files. */
|
|
25
|
+
export function assertEmbedOutputPaths(dir, paths) {
|
|
26
|
+
for (const path of paths) {
|
|
27
|
+
let current = dir;
|
|
28
|
+
for (const segment of ['', ...path.split('/')]) {
|
|
29
|
+
current = join(current, segment);
|
|
30
|
+
if (lstatSync(current, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
31
|
+
throw new Error('assistant_embed_unsafe_path');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function applyEmbedInstallation(dir, contents, files) {
|
|
37
|
+
assertEmbedOutputPaths(dir, files.map((file) => file.path));
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (file.action === 'unchanged' || file.action === 'skipped')
|
|
40
|
+
continue;
|
|
41
|
+
const full = join(dir, file.path);
|
|
42
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
43
|
+
const content = contents[file.path];
|
|
44
|
+
if (content === undefined || hash(content) !== file.sha256) {
|
|
45
|
+
throw new Error('assistant_embed_plan_changed');
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(full, content, {
|
|
48
|
+
flag: file.action === 'created' ? 'wx' : 'w',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Tool-facing recipe metadata describes installed assets, never a parallel authoring language. */
|
|
53
|
+
export function embedRecipe(framework, surface, files) {
|
|
54
|
+
return {
|
|
55
|
+
id: `${framework}-${surface}`,
|
|
56
|
+
version: 1,
|
|
57
|
+
fingerprint: hash(JSON.stringify(files.map(({ path, sha256 }) => ({ path, sha256 })))),
|
|
58
|
+
appliesTo: [
|
|
59
|
+
'Existing application with the requested website surface',
|
|
60
|
+
'One canonical TypeScript Noodle application',
|
|
61
|
+
],
|
|
62
|
+
doesNotApplyWhen: [
|
|
63
|
+
'Native/mobile host',
|
|
64
|
+
'No authorized access to the customer application',
|
|
65
|
+
'Unresolved identity or tenant authorization',
|
|
66
|
+
],
|
|
67
|
+
dependencies: framework === 'django-vue'
|
|
68
|
+
? ['@noodleseed/assistant', 'Django', 'requests', 'vue']
|
|
69
|
+
: ['@noodleseed/assistant'],
|
|
70
|
+
applicationSeams: surface === 'public'
|
|
71
|
+
? ['Public embed configuration', 'Application mount', 'Existing business operations']
|
|
72
|
+
: [
|
|
73
|
+
framework === 'django-vue'
|
|
74
|
+
? 'authenticate_assistant_request'
|
|
75
|
+
: 'authenticateAssistantRequest',
|
|
76
|
+
'Backend-owned tenant and permission mapping',
|
|
77
|
+
...(surface === 'mixed' ? ['Existing login transaction and signInTicket handoff'] : []),
|
|
78
|
+
'Application mount',
|
|
79
|
+
'Existing business operations',
|
|
80
|
+
],
|
|
81
|
+
ownership: 'Customer-owned source after installation; reruns preserve modified files unless --force is explicitly selected.',
|
|
82
|
+
verification: [
|
|
83
|
+
'Host build',
|
|
84
|
+
'Application identity/tenant negative tests',
|
|
85
|
+
'Representative sandbox workflow',
|
|
86
|
+
'Browser turn and linked App',
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function embedNextSteps(surface, framework = 'nextjs') {
|
|
91
|
+
if (framework === 'django-vue')
|
|
92
|
+
return [
|
|
93
|
+
'Reuse the Django and Vue package managers; add requests and a compatible @noodleseed/assistant.',
|
|
94
|
+
'Include noodle_assistant.urls and bind the server-only names in noodle_assistant/settings.example.py.',
|
|
95
|
+
'Implement authenticate_assistant_request in noodle_assistant/auth.py using existing session and membership checks.',
|
|
96
|
+
'Render src/components/NoodleAssistant.vue with principalKey and the existing application csrfToken.',
|
|
97
|
+
'Keep normal Django CSRF middleware; route the session endpoint through the frontend origin.',
|
|
98
|
+
'Run python manage.py test noodle_assistant, the generated frontend transport tests, and the existing Vue build.',
|
|
99
|
+
'Run noodle assistant embed --framework django-vue --check --json with settings exported; finish the independent checks in NOODLE-INTEGRATION.md.',
|
|
100
|
+
'Integration remains unverified until actual application identity, tenant and browser checks pass.',
|
|
101
|
+
];
|
|
102
|
+
return [
|
|
103
|
+
'Install a compatible @noodleseed/assistant version with the application package manager.',
|
|
104
|
+
...(surface === 'public'
|
|
105
|
+
? [
|
|
106
|
+
'Set NEXT_PUBLIC_NOODLE_EMBED_ID and NEXT_PUBLIC_NOODLE_SERVICE_URL from the selected local or hosted deployment.',
|
|
107
|
+
]
|
|
108
|
+
: [
|
|
109
|
+
'Configure backend-only credentials from noodle assistant clients create for an explicitly authorized target.',
|
|
110
|
+
'Implement authenticateAssistantRequest in lib/noodle-assistant-auth.ts using the existing session and membership checks.',
|
|
111
|
+
]),
|
|
112
|
+
...(surface === 'mixed'
|
|
113
|
+
? [
|
|
114
|
+
'Bind onSignInRequested to the existing backend login transaction; verify signInTicket continuation.',
|
|
115
|
+
]
|
|
116
|
+
: []),
|
|
117
|
+
surface === 'public'
|
|
118
|
+
? 'Render AssistantWidget in the selected application layout.'
|
|
119
|
+
: 'Render AssistantWidget with a stable user/tenant principalKey; change it on account changes (null for an anonymous mixed surface).',
|
|
120
|
+
`Run ${embedCheckCommand(surface)} with the host environment exported, then the independent checks in NOODLE-INTEGRATION.md.`,
|
|
121
|
+
'Application integration remains unverified until those checks run; installation does not authorize deployment or live writes.',
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=assistant-embed-installation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-installation.js","sourceRoot":"","sources":["../src/assistant-embed-installation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EmbedSurface } from './assistant-embed-scaffold-template.js';
|
|
2
|
+
/** Reuse preflight's explicit env aliases instead of duplicating browser configuration. */
|
|
3
|
+
export declare function embedCheckCommand(surface: EmbedSurface): string;
|
|
4
|
+
/** Public mounting uses the existing hosted session source, never a customer proxy route. */
|
|
5
|
+
export declare function publicEmbedFiles(surface: 'public' | 'mixed'): Record<string, string>;
|
|
6
|
+
/** One customer handoff accompanies every installed profile and its machine-readable plan. */
|
|
7
|
+
export declare function embedIntegrationGuide(surface: EmbedSurface): string;
|
|
8
|
+
//# sourceMappingURL=assistant-embed-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-embed-profile.d.ts","sourceRoot":"","sources":["../src/assistant-embed-profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAG3E,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAQ/D;AAED,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4EpF;AAED,8FAA8F;AAC9F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAiEnE"}
|