@chatlychat/cli 0.4.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/commands/auth.d.ts +1 -1
  2. package/dist/commands/auth.d.ts.map +1 -1
  3. package/dist/commands/auth.js +40 -3
  4. package/dist/commands/auth.js.map +1 -1
  5. package/dist/commands/deliveries.d.ts +18 -0
  6. package/dist/commands/deliveries.d.ts.map +1 -0
  7. package/dist/commands/deliveries.js +46 -0
  8. package/dist/commands/deliveries.js.map +1 -0
  9. package/dist/commands/init.d.ts +29 -0
  10. package/dist/commands/init.d.ts.map +1 -0
  11. package/dist/commands/init.js +176 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/integrations.d.ts +27 -0
  14. package/dist/commands/integrations.d.ts.map +1 -1
  15. package/dist/commands/integrations.js +145 -4
  16. package/dist/commands/integrations.js.map +1 -1
  17. package/dist/commands/platform-as-code.d.ts +54 -0
  18. package/dist/commands/platform-as-code.d.ts.map +1 -0
  19. package/dist/commands/platform-as-code.js +185 -0
  20. package/dist/commands/platform-as-code.js.map +1 -0
  21. package/dist/commands/workspace-as-code.d.ts +52 -11
  22. package/dist/commands/workspace-as-code.d.ts.map +1 -1
  23. package/dist/commands/workspace-as-code.js +70 -189
  24. package/dist/commands/workspace-as-code.js.map +1 -1
  25. package/dist/help.d.ts +1 -1
  26. package/dist/help.d.ts.map +1 -1
  27. package/dist/help.js +36 -1
  28. package/dist/help.js.map +1 -1
  29. package/dist/init/shared.d.ts +61 -0
  30. package/dist/init/shared.d.ts.map +1 -0
  31. package/dist/init/shared.js +110 -0
  32. package/dist/init/shared.js.map +1 -0
  33. package/dist/init/templates.d.ts +29 -0
  34. package/dist/init/templates.d.ts.map +1 -0
  35. package/dist/init/templates.js +362 -0
  36. package/dist/init/templates.js.map +1 -0
  37. package/dist/pac/platform-document.d.ts +65 -0
  38. package/dist/pac/platform-document.d.ts.map +1 -0
  39. package/dist/pac/platform-document.js +149 -0
  40. package/dist/pac/platform-document.js.map +1 -0
  41. package/dist/pac/platform-live.d.ts +65 -0
  42. package/dist/pac/platform-live.d.ts.map +1 -0
  43. package/dist/pac/platform-live.js +263 -0
  44. package/dist/pac/platform-live.js.map +1 -0
  45. package/dist/pac/platform-resources.d.ts +141 -0
  46. package/dist/pac/platform-resources.d.ts.map +1 -0
  47. package/dist/pac/platform-resources.js +509 -0
  48. package/dist/pac/platform-resources.js.map +1 -0
  49. package/dist/run.d.ts.map +1 -1
  50. package/dist/run.js +26 -2
  51. package/dist/run.js.map +1 -1
  52. package/dist/wac/apply.d.ts.map +1 -1
  53. package/dist/wac/apply.js +56 -16
  54. package/dist/wac/apply.js.map +1 -1
  55. package/dist/wac/diff-warnings.d.ts +35 -0
  56. package/dist/wac/diff-warnings.d.ts.map +1 -0
  57. package/dist/wac/diff-warnings.js +88 -0
  58. package/dist/wac/diff-warnings.js.map +1 -0
  59. package/dist/wac/diff.d.ts +40 -70
  60. package/dist/wac/diff.d.ts.map +1 -1
  61. package/dist/wac/diff.js +114 -91
  62. package/dist/wac/diff.js.map +1 -1
  63. package/dist/wac/document-key.d.ts +30 -0
  64. package/dist/wac/document-key.d.ts.map +1 -0
  65. package/dist/wac/document-key.js +87 -0
  66. package/dist/wac/document-key.js.map +1 -0
  67. package/dist/wac/document.d.ts +75 -65
  68. package/dist/wac/document.d.ts.map +1 -1
  69. package/dist/wac/document.js +60 -46
  70. package/dist/wac/document.js.map +1 -1
  71. package/dist/wac/execute.d.ts +25 -0
  72. package/dist/wac/execute.d.ts.map +1 -0
  73. package/dist/wac/execute.js +126 -0
  74. package/dist/wac/execute.js.map +1 -0
  75. package/dist/wac/load.d.ts +17 -0
  76. package/dist/wac/load.d.ts.map +1 -0
  77. package/dist/wac/load.js +92 -0
  78. package/dist/wac/load.js.map +1 -0
  79. package/dist/wac/resource-def.d.ts +89 -1
  80. package/dist/wac/resource-def.d.ts.map +1 -1
  81. package/dist/wac/resource-def.js +4 -0
  82. package/dist/wac/resource-def.js.map +1 -1
  83. package/dist/wac/resources.d.ts +9 -11
  84. package/dist/wac/resources.d.ts.map +1 -1
  85. package/dist/wac/resources.js +93 -27
  86. package/dist/wac/resources.js.map +1 -1
  87. package/dist/wac/values.d.ts +20 -0
  88. package/dist/wac/values.d.ts.map +1 -1
  89. package/dist/wac/values.js +46 -0
  90. package/dist/wac/values.js.map +1 -1
  91. package/package.json +4 -3
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The parts of `chatly init` (CHT-F02) that do not depend on the framework:
3
+ * the CSP module and the privacy-policy table. Both are rendered from the
4
+ * channel's own `embed` block, read off `GET /v1/channels` with the caller's
5
+ * key — the same block the dashboard's install screen shows — so neither is a
6
+ * hand-kept copy of something the server already says.
7
+ */
8
+ /**
9
+ * The module every framework's CSP is merged from.
10
+ *
11
+ * `.mjs` and dependency-free on purpose: `next.config.{js,mjs,ts}` and an
12
+ * Express server can both import it, and a header built from it needs no
13
+ * library. It does NOT write a header itself — the host page already has a
14
+ * policy, and a second `Content-Security-Policy` header INTERSECTS with the
15
+ * first rather than extending it, which would block the host's own scripts.
16
+ * So it merges into the directives the integrator already has.
17
+ */
18
+ export function cspModule(input) {
19
+ const csp = input.embed.contentSecurityPolicy;
20
+ const list = (values) => `[${values.map((v) => JSON.stringify(v)).join(', ')}]`;
21
+ return {
22
+ path: 'chatly.csp.mjs',
23
+ contents: `/**
24
+ * Chatly's Content-Security-Policy sources for the web channel ${input.publicId}.
25
+ *
26
+ * Generated by \`chatly init\` on ${input.generatedOn} from the channel's own
27
+ * \`embed.contentSecurityPolicy\`. Re-run \`chatly init\` (or read
28
+ * \`GET /v1/channels\`) when Chatly announces a host change — the lists are
29
+ * the origins CHATLY serves from, not an image URL an operator put in a theme.
30
+ *
31
+ * MERGE these into your existing directives; do not send a second
32
+ * Content-Security-Policy header. Two headers intersect, so a second one
33
+ * would block your own scripts rather than allow Chatly's.
34
+ *
35
+ * The embed snippet is an inline <script>: your script-src needs a nonce or
36
+ * hash for it, or load the widget with @chatlychat/widget-react instead.
37
+ */
38
+ export const chatlyCsp = {
39
+ 'script-src': ${list(csp.scriptSrc)},
40
+ 'connect-src': ${list(csp.connectSrc)},
41
+ 'img-src': ${list(csp.imgSrc)},
42
+ 'media-src': ${list(csp.mediaSrc)},
43
+ };
44
+
45
+ /**
46
+ * Your directives with Chatly's sources appended, de-duplicated.
47
+ * Directive names are the header's own (\`'script-src'\`, not \`scriptSrc\`).
48
+ *
49
+ * @param {Record<string, string[]>} directives
50
+ * @returns {Record<string, string[]>}
51
+ */
52
+ export function withChatly(directives) {
53
+ const out = { ...directives };
54
+ for (const [name, sources] of Object.entries(chatlyCsp)) {
55
+ out[name] = [...new Set([...(out[name] ?? ["'self'"]), ...sources])];
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Directives to a header value: \`"script-src 'self' https://…; connect-src …"\`.
62
+ *
63
+ * @param {Record<string, string[]>} directives
64
+ * @returns {string}
65
+ */
66
+ export function toHeader(directives) {
67
+ return Object.entries(directives)
68
+ .map(([name, sources]) => [name, ...sources].join(' '))
69
+ .join('; ');
70
+ }
71
+ `,
72
+ };
73
+ }
74
+ /** Escape a value for a Markdown table cell. */
75
+ function cell(value) {
76
+ return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
77
+ }
78
+ /**
79
+ * The privacy-policy table (CHT-DOC12's content, rendered for this channel).
80
+ *
81
+ * Every key with the channel's `publicId` already substituted, which is the
82
+ * form a privacy policy or a consent tool's cookie scan needs. `cookies` is
83
+ * stated as none because the declaration says so — an omitted row reads as
84
+ * "not declared", not as "none".
85
+ */
86
+ export function privacyTable(input) {
87
+ const rows = input.embed.storage.map((k) => `| \`${cell(k.key)}\` | ${cell(k.store)} | ${cell(k.purpose)} | ${cell(k.lifetime)} | ` +
88
+ `${cell(k.category)} | ${cell(k.writtenWhen)} |`);
89
+ return {
90
+ path: 'CHATLY-PRIVACY.md',
91
+ contents: `# What the Chatly widget stores in a visitor's browser
92
+
93
+ Generated by \`chatly init\` on ${input.generatedOn} from the storage
94
+ declaration of web channel \`${input.publicId}\` — the same list Chatly
95
+ publishes for that channel without a key. Paste the table into your privacy
96
+ policy or cookie notice.
97
+
98
+ **Cookies: none.** The widget sets no cookies.
99
+
100
+ | Key | Store | Purpose | Lifetime | Category | Written when |
101
+ | --- | --- | --- | --- | --- | --- |
102
+ ${rows.join('\n')}
103
+
104
+ Whether ANY of these is written is decided by the \`storage\` option on your
105
+ embed, which the server never sees — \`storage: 'deny'\` writes none of them.
106
+ See https://chatlychat.com/guides/consent.
107
+ `,
108
+ };
109
+ }
110
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/init/shared.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuCH;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,KAAoB;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC;IAC9C,MAAM,IAAI,GAAG,CAAC,MAA6B,EAAU,EAAE,CACrD,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACzD,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,QAAQ,EAAE;kEACoD,KAAK,CAAC,QAAQ;;qCAE3C,KAAK,CAAC,WAAW;;;;;;;;;;;;;kBAapC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;mBAClB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;eACxB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;iBACd,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BlC;KACE,CAAC;AACJ,CAAC;AAED,gDAAgD;AAChD,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAoB;IAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAClC,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK;QACvF,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CACnD,CAAC;IACF,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE;;kCAEoB,KAAK,CAAC,WAAW;+BACpB,KAAK,CAAC,QAAQ;;;;;;;;EAQ3C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;CAKhB;KACE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The framework templates for `chatly init` (CHT-F02).
3
+ *
4
+ * Every API a generated file uses is in a PUBLISHED package at the version on
5
+ * npm today — `chatlyUserHash` from `@chatlychat/widget-react/server`,
6
+ * `<ChatlyLauncher>`'s `externalId`/`userHash`/`getToken` props, and
7
+ * `Chatly('identify', …)` on the snippet. A template that imported something
8
+ * from a working tree would scaffold a project that does not install.
9
+ *
10
+ * ═══ THE ONE THING A WIZARD CANNOT KNOW ═══════════════════════════════════
11
+ *
12
+ * Who is signed in. Every identity route below has a `currentUserId` stub
13
+ * that answers `null`, so a freshly scaffolded app runs with every visitor
14
+ * ANONYMOUS — never with an unsigned claim. Wiring it to the integrator's
15
+ * auth is the step the summary names first.
16
+ */
17
+ import { type ScaffoldFile, type ScaffoldInput } from './shared.js';
18
+ export declare const FRAMEWORKS: readonly ["next", "remix", "astro", "express"];
19
+ export type Framework = (typeof FRAMEWORKS)[number];
20
+ /**
21
+ * The frameworks with no `--identity atlas` output. An Atlas token is minted
22
+ * in the browser, so there is no server half to generate, and these two have
23
+ * no React component to pass `getToken` to — the page calls
24
+ * `Chatly('identify', { getToken })` itself.
25
+ */
26
+ export declare const NO_ATLAS: ReadonlyArray<Framework>;
27
+ /** Every file `chatly init <framework>` writes, in the order it reports them. */
28
+ export declare function scaffold(framework: Framework, input: ScaffoldInput): ReadonlyArray<ScaffoldFile>;
29
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/init/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAA2B,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7F,eAAO,MAAM,UAAU,gDAAiD,CAAC;AACzE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,aAAa,CAAC,SAAS,CAAwB,CAAC;AAsTvE,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,GAAG,aAAa,CAAC,YAAY,CAAC,CAwChG"}
@@ -0,0 +1,362 @@
1
+ /**
2
+ * The framework templates for `chatly init` (CHT-F02).
3
+ *
4
+ * Every API a generated file uses is in a PUBLISHED package at the version on
5
+ * npm today — `chatlyUserHash` from `@chatlychat/widget-react/server`,
6
+ * `<ChatlyLauncher>`'s `externalId`/`userHash`/`getToken` props, and
7
+ * `Chatly('identify', …)` on the snippet. A template that imported something
8
+ * from a working tree would scaffold a project that does not install.
9
+ *
10
+ * ═══ THE ONE THING A WIZARD CANNOT KNOW ═══════════════════════════════════
11
+ *
12
+ * Who is signed in. Every identity route below has a `currentUserId` stub
13
+ * that answers `null`, so a freshly scaffolded app runs with every visitor
14
+ * ANONYMOUS — never with an unsigned claim. Wiring it to the integrator's
15
+ * auth is the step the summary names first.
16
+ */
17
+ import { cspModule, privacyTable } from './shared.js';
18
+ export const FRAMEWORKS = ['next', 'remix', 'astro', 'express'];
19
+ /**
20
+ * The frameworks with no `--identity atlas` output. An Atlas token is minted
21
+ * in the browser, so there is no server half to generate, and these two have
22
+ * no React component to pass `getToken` to — the page calls
23
+ * `Chatly('identify', { getToken })` itself.
24
+ */
25
+ export const NO_ATLAS = ['express', 'astro'];
26
+ const IDENTITY_ROUTE_NEXT = (input) => `/**
27
+ * Chatly identity verification (HMAC) for web channel ${input.publicId}.
28
+ * Generated by \`chatly init next\` on ${input.generatedOn}.
29
+ *
30
+ * Signs the signed-in user's id with the channel's IDV secret, so the widget
31
+ * can prove who the visitor is. The secret stays on this server:
32
+ * \`@chatlychat/widget-react/server\` refuses to run in a browser bundle.
33
+ *
34
+ * Wire \`currentUserId\` to your auth before shipping. Until then it answers
35
+ * \`{ identity: null }\` and every visitor chats anonymously.
36
+ */
37
+ import { chatlyUserHash } from '@chatlychat/widget-react/server';
38
+ import { NextResponse } from 'next/server';
39
+
40
+ export const dynamic = 'force-dynamic';
41
+
42
+ const NO_STORE = { 'cache-control': 'no-store' };
43
+
44
+ /** TODO: return the signed-in user's stable id from your auth library, or null. */
45
+ async function currentUserId(): Promise<string | null> {
46
+ return null;
47
+ }
48
+
49
+ export async function GET(): Promise<NextResponse> {
50
+ const externalId = await currentUserId();
51
+ if (externalId === null) {
52
+ return NextResponse.json({ identity: null }, { headers: NO_STORE });
53
+ }
54
+ const secret = process.env.CHATLY_IDV_SECRET;
55
+ if (secret === undefined || secret === '') {
56
+ // Loud rather than anonymous: a signed-in user silently chatting as a
57
+ // stranger is a misconfiguration nobody would notice.
58
+ return NextResponse.json(
59
+ { error: 'CHATLY_IDV_SECRET is not set on the server' },
60
+ { status: 500, headers: NO_STORE },
61
+ );
62
+ }
63
+ return NextResponse.json(
64
+ { identity: { externalId, userHash: chatlyUserHash(secret, externalId) } },
65
+ { headers: NO_STORE },
66
+ );
67
+ }
68
+ `;
69
+ const LAUNCHER_NEXT_HMAC = (input) => `'use client';
70
+ /**
71
+ * The Chatly launcher for web channel ${input.publicId}, identified through
72
+ * \`app/api/chatly/identity/route.ts\`. Generated by \`chatly init next\` on
73
+ * ${input.generatedOn}. Render <Chatly /> once, in your root layout.
74
+ */
75
+ import { ChatlyLauncher } from '@chatlychat/widget-react';
76
+ import { useEffect, useState } from 'react';
77
+
78
+ interface Identity {
79
+ readonly externalId: string;
80
+ readonly userHash: string;
81
+ }
82
+
83
+ export function Chatly() {
84
+ // undefined = still asking; null = anonymous.
85
+ const [identity, setIdentity] = useState<Identity | null | undefined>(undefined);
86
+
87
+ useEffect(() => {
88
+ let live = true;
89
+ fetch('/api/chatly/identity', { cache: 'no-store' })
90
+ .then(async (r) => (r.ok ? ((await r.json()) as { identity: Identity | null }) : { identity: null }))
91
+ .then((body) => live && setIdentity(body.identity))
92
+ .catch(() => live && setIdentity(null));
93
+ return () => {
94
+ live = false;
95
+ };
96
+ }, []);
97
+
98
+ // Rendered only once the identity is known, so the widget starts as the
99
+ // right visitor instead of starting anonymous and switching.
100
+ if (identity === undefined) {
101
+ return null;
102
+ }
103
+ return <ChatlyLauncher publicId=${JSON.stringify(input.publicId)} {...(identity ?? {})} />;
104
+ }
105
+ `;
106
+ const LAUNCHER_NEXT_ATLAS = (input) => `'use client';
107
+ /**
108
+ * The Chatly launcher for web channel ${input.publicId}, identified with an
109
+ * Atlas token (\`identityVerification.mode: 'atlas'\`). Generated by
110
+ * \`chatly init next --identity atlas\` on ${input.generatedOn}.
111
+ *
112
+ * Pass \`getToken\` for a signed-in user and leave it out for a signed-out one:
113
+ *
114
+ * <Chatly getToken={() => atlas.session.getToken({ template: 'chatly' })} />
115
+ *
116
+ * The template and the channel setting it must match are on
117
+ * https://chatlychat.com/guides/identity-verification#worked-example-an-atlas-instance-on-the-shared-host
118
+ */
119
+ import { ChatlyLauncher } from '@chatlychat/widget-react';
120
+
121
+ export function Chatly({ getToken }: { getToken?: () => Promise<string> }) {
122
+ return (
123
+ <ChatlyLauncher
124
+ publicId=${JSON.stringify(input.publicId)}
125
+ {...(getToken === undefined ? {} : { getToken })}
126
+ />
127
+ );
128
+ }
129
+ `;
130
+ const IDENTITY_EXPRESS = (input) => `/**
131
+ * Chatly identity verification (HMAC) for web channel ${input.publicId}.
132
+ * Generated by \`chatly init express\` on ${input.generatedOn}.
133
+ *
134
+ * import { chatlyIdentity } from './chatly/identity.mjs';
135
+ * app.get('/api/chatly/identity', chatlyIdentity((req) => req.user?.id ?? null));
136
+ *
137
+ * \`userIdOf\` is the one thing to write: return the signed-in user's stable id,
138
+ * or null for a signed-out visitor, who then chats anonymously.
139
+ */
140
+ import { createHmac } from 'node:crypto';
141
+
142
+ /** HMAC-SHA256(secret, externalId), hex — what the widget sends as userHash. */
143
+ export function chatlyUserHash(secret, externalId) {
144
+ return createHmac('sha256', secret).update(externalId).digest('hex');
145
+ }
146
+
147
+ /** @param {(req: any) => string | null | Promise<string | null>} userIdOf */
148
+ export function chatlyIdentity(userIdOf) {
149
+ return async (req, res) => {
150
+ res.set('cache-control', 'no-store');
151
+ const externalId = await userIdOf(req);
152
+ if (externalId === null || externalId === undefined) {
153
+ return res.json({ identity: null });
154
+ }
155
+ const secret = process.env.CHATLY_IDV_SECRET;
156
+ if (!secret) {
157
+ return res.status(500).json({ error: 'CHATLY_IDV_SECRET is not set on the server' });
158
+ }
159
+ return res.json({ identity: { externalId, userHash: chatlyUserHash(secret, externalId) } });
160
+ };
161
+ }
162
+ `;
163
+ const SNIPPET_EXPRESS = (input) => `<!--
164
+ Chatly for web channel ${input.publicId}. Generated by \`chatly init express\`
165
+ on ${input.generatedOn}. Paste before </body>. The first script is the
166
+ channel's own embed snippet; the second identifies a signed-in visitor
167
+ through /api/chatly/identity (see chatly/identity.mjs).
168
+ -->
169
+ ${input.embed.snippet}
170
+ <script>
171
+ fetch('/api/chatly/identity', { cache: 'no-store' })
172
+ .then(function (r) { return r.ok ? r.json() : { identity: null }; })
173
+ .then(function (body) { if (body.identity) Chatly('identify', body.identity); })
174
+ .catch(function () {});
175
+ </script>
176
+ `;
177
+ /**
178
+ * The Remix loader imports nothing from Remix or React Router on purpose. A
179
+ * `LoaderFunctionArgs` import would pin the file to `@remix-run/node` (Remix
180
+ * v2) or to `react-router` (v7 framework mode), and a loader that takes
181
+ * `{ request }` and returns a `Response` is a valid loader in both.
182
+ */
183
+ const IDENTITY_ROUTE_REMIX = (input) => `/**
184
+ * Chatly identity verification (HMAC) for web channel ${input.publicId}.
185
+ * Generated by \`chatly init remix\` on ${input.generatedOn}.
186
+ *
187
+ * A resource route (a loader and no component), served at
188
+ * /api/chatly/identity by Remix v2's flat routes and by React Router v7's
189
+ * \`flatRoutes()\`. Signs the signed-in user's id with the channel's IDV
190
+ * secret, so the widget can prove who the visitor is. The secret stays on
191
+ * this server: a loader never reaches the browser bundle, and
192
+ * \`@chatlychat/widget-react/server\` refuses to run in one anyway.
193
+ *
194
+ * Wire \`currentUserId\` to your auth before shipping. Until then it answers
195
+ * \`{ identity: null }\` and every visitor chats anonymously.
196
+ */
197
+ import { chatlyUserHash } from '@chatlychat/widget-react/server';
198
+
199
+ const HEADERS = { 'content-type': 'application/json', 'cache-control': 'no-store' };
200
+
201
+ /** TODO: return the signed-in user's stable id from your session, or null. */
202
+ async function currentUserId(request: Request): Promise<string | null> {
203
+ void request;
204
+ return null;
205
+ }
206
+
207
+ export async function loader({ request }: { request: Request }): Promise<Response> {
208
+ const externalId = await currentUserId(request);
209
+ if (externalId === null) {
210
+ return new Response(JSON.stringify({ identity: null }), { headers: HEADERS });
211
+ }
212
+ const secret = process.env.CHATLY_IDV_SECRET;
213
+ if (secret === undefined || secret === '') {
214
+ // Loud rather than anonymous: a signed-in user silently chatting as a
215
+ // stranger is a misconfiguration nobody would notice.
216
+ return new Response(JSON.stringify({ error: 'CHATLY_IDV_SECRET is not set on the server' }), {
217
+ status: 500,
218
+ headers: HEADERS,
219
+ });
220
+ }
221
+ return new Response(
222
+ JSON.stringify({ identity: { externalId, userHash: chatlyUserHash(secret, externalId) } }),
223
+ { headers: HEADERS },
224
+ );
225
+ }
226
+ `;
227
+ /**
228
+ * Remix's launcher is Next's without `'use client'` and with its own header:
229
+ * every Remix component renders on the server and hydrates, and the identity
230
+ * fetch is in an effect, so it runs in the browser only.
231
+ */
232
+ const LAUNCHER_REMIX_HMAC = (input) => LAUNCHER_NEXT_HMAC(input)
233
+ .replace(/^'use client';\n/, '')
234
+ .replace('`app/api/chatly/identity/route.ts`', '`app/routes/api.chatly.identity.ts`')
235
+ .replace('`chatly init next`', '`chatly init remix`')
236
+ .replace('in your root layout', 'in app/root.tsx');
237
+ const LAUNCHER_REMIX_ATLAS = (input) => LAUNCHER_NEXT_ATLAS(input)
238
+ .replace(/^'use client';\n/, '')
239
+ .replace('`chatly init next --identity atlas`', '`chatly init remix --identity atlas`');
240
+ /**
241
+ * Astro's endpoint signs with `node:crypto`, like Express's, rather than
242
+ * importing `@chatlychat/widget-react/server`: an Astro site has no reason to
243
+ * install a React package, and the construction is one line.
244
+ */
245
+ const IDENTITY_ROUTE_ASTRO = (input) => `/**
246
+ * Chatly identity verification (HMAC) for web channel ${input.publicId}.
247
+ * Generated by \`chatly init astro\` on ${input.generatedOn}.
248
+ *
249
+ * Served at /api/chatly/identity. \`prerender = false\` makes it run per
250
+ * request, which needs a server adapter with Node built-ins
251
+ * (\`@astrojs/node\`, or another adapter with \`node:crypto\`): a prerendered
252
+ * identity would be one visitor's answer frozen into a static file.
253
+ *
254
+ * Wire \`currentUserId\` to your auth before shipping. Until then it answers
255
+ * \`{ identity: null }\` and every visitor chats anonymously.
256
+ */
257
+ import { createHmac } from 'node:crypto';
258
+
259
+ import type { APIRoute } from 'astro';
260
+
261
+ export const prerender = false;
262
+
263
+ const HEADERS = { 'content-type': 'application/json', 'cache-control': 'no-store' };
264
+
265
+ /** HMAC-SHA256(secret, externalId), hex — what the widget sends as userHash. */
266
+ export function chatlyUserHash(secret: string, externalId: string): string {
267
+ return createHmac('sha256', secret).update(externalId).digest('hex');
268
+ }
269
+
270
+ /** TODO: return the signed-in user's stable id (from \`locals\`, a cookie, …), or null. */
271
+ async function currentUserId(context: Parameters<APIRoute>[0]): Promise<string | null> {
272
+ void context;
273
+ return null;
274
+ }
275
+
276
+ export const GET: APIRoute = async (context) => {
277
+ const externalId = await currentUserId(context);
278
+ if (externalId === null) {
279
+ return new Response(JSON.stringify({ identity: null }), { headers: HEADERS });
280
+ }
281
+ const secret = process.env.CHATLY_IDV_SECRET;
282
+ if (secret === undefined || secret === '') {
283
+ // Loud rather than anonymous: a signed-in user silently chatting as a
284
+ // stranger is a misconfiguration nobody would notice.
285
+ return new Response(JSON.stringify({ error: 'CHATLY_IDV_SECRET is not set on the server' }), {
286
+ status: 500,
287
+ headers: HEADERS,
288
+ });
289
+ }
290
+ return new Response(
291
+ JSON.stringify({ identity: { externalId, userHash: chatlyUserHash(secret, externalId) } }),
292
+ { headers: HEADERS },
293
+ );
294
+ };
295
+ `;
296
+ /**
297
+ * The snippet is a string in the frontmatter and rendered with `set:html`, not
298
+ * pasted into the template: a bare `<script>` in an `.astro` file is bundled
299
+ * and turned into a module, which would move the shim off `window` and out of
300
+ * order. `set:html` emits it byte for byte, and `is:inline` does the same for
301
+ * the identify call.
302
+ */
303
+ const COMPONENT_ASTRO = (input) => `---
304
+ /**
305
+ * Chatly for web channel ${input.publicId}. Generated by \`chatly init astro\`
306
+ * on ${input.generatedOn}. Render <Chatly /> once, before </body> in your
307
+ * layout. The first script is the channel's own embed snippet; the second
308
+ * identifies a signed-in visitor through /api/chatly/identity
309
+ * (src/pages/api/chatly/identity.ts).
310
+ */
311
+ const snippet = ${JSON.stringify(input.embed.snippet)};
312
+ ---
313
+
314
+ <Fragment set:html={snippet} />
315
+ <script is:inline>
316
+ fetch('/api/chatly/identity', { cache: 'no-store' })
317
+ .then(function (r) { return r.ok ? r.json() : { identity: null }; })
318
+ .then(function (body) { if (body.identity) Chatly('identify', body.identity); })
319
+ .catch(function () {});
320
+ </script>
321
+ `;
322
+ /** Every file `chatly init <framework>` writes, in the order it reports them. */
323
+ export function scaffold(framework, input) {
324
+ const common = [cspModule(input), privacyTable(input)];
325
+ if (framework === 'next') {
326
+ return [
327
+ ...(input.identity === 'hmac'
328
+ ? [{ path: 'app/api/chatly/identity/route.ts', contents: IDENTITY_ROUTE_NEXT(input) }]
329
+ : []),
330
+ {
331
+ path: 'components/chatly.tsx',
332
+ contents: input.identity === 'hmac' ? LAUNCHER_NEXT_HMAC(input) : LAUNCHER_NEXT_ATLAS(input),
333
+ },
334
+ ...common,
335
+ ];
336
+ }
337
+ if (framework === 'remix') {
338
+ return [
339
+ ...(input.identity === 'hmac'
340
+ ? [{ path: 'app/routes/api.chatly.identity.ts', contents: IDENTITY_ROUTE_REMIX(input) }]
341
+ : []),
342
+ {
343
+ path: 'app/components/chatly.tsx',
344
+ contents: input.identity === 'hmac' ? LAUNCHER_REMIX_HMAC(input) : LAUNCHER_REMIX_ATLAS(input),
345
+ },
346
+ ...common,
347
+ ];
348
+ }
349
+ if (framework === 'astro') {
350
+ return [
351
+ { path: 'src/pages/api/chatly/identity.ts', contents: IDENTITY_ROUTE_ASTRO(input) },
352
+ { path: 'src/components/Chatly.astro', contents: COMPONENT_ASTRO(input) },
353
+ ...common,
354
+ ];
355
+ }
356
+ return [
357
+ { path: 'chatly/identity.mjs', contents: IDENTITY_EXPRESS(input) },
358
+ { path: 'chatly/snippet.html', contents: SNIPPET_EXPRESS(input) },
359
+ ...common,
360
+ ];
361
+ }
362
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/init/templates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `chatly.platform.json` — the platform document's schema.
3
+ *
4
+ * ═══ WHAT IT LOOKS LIKE ═══════════════════════════════════════════════════
5
+ *
6
+ * {
7
+ * "version": 1,
8
+ * "platform": { "prefix": "cp_live_a1b2c3d4" },
9
+ * "resources": {
10
+ * "workspaces": [{ "slug": "acme", "name": "Acme Inc" }],
11
+ * "users": [
12
+ * { "workspace": "acme", "email": "ops@acme.test", "name": "Ops", "role": "admin" }
13
+ * ],
14
+ * "apiKeys": [
15
+ * { "workspace": "acme", "name": "deploy", "scopes": ["conversations:read"] }
16
+ * ],
17
+ * "botCredentials": [{ "workspace": "acme", "name": "assistant" }],
18
+ * "knowledgeSources": [
19
+ * { "workspace": "acme", "label": "Docs", "url": "https://acme.test/docs" }
20
+ * ]
21
+ * }
22
+ * }
23
+ *
24
+ * ═══ WHY FLAT SECTIONS AND NOT WORKSPACES THAT NEST THEIR CHILDREN ════════
25
+ *
26
+ * A nested shape reads better for one tenant and worse for the job this file
27
+ * exists for. The ask behind CHT-F04 is "four products were provisioned four
28
+ * slightly different ways", which is a question about CONSISTENCY ACROSS
29
+ * tenants: what every workspace's deploy key is scoped to, which of them has
30
+ * a bot credential nobody uses, whether the same two people are admins
31
+ * everywhere. Flat sections put each of those on one screen and make a
32
+ * diff-of-the-file readable; nesting buries them one indent deeper per
33
+ * tenant.
34
+ *
35
+ * It also means the engine is the same engine. `wac/diff.ts` and
36
+ * `wac/apply.ts` are untouched by this feature except for the path-parameter
37
+ * mechanism, and the dependency ordering a nested document would have had to
38
+ * re-invent (create the workspace, then everything that names it, and skip a
39
+ * child whose parent's create failed) is what `references` and `dependsOn`
40
+ * already do.
41
+ *
42
+ * ═══ THE GUARD ════════════════════════════════════════════════════════════
43
+ *
44
+ * `platform.prefix` against `GET /v1/platform/whoami`'s `prefix`, which is
45
+ * the identifying head of the `cp_` token. The workspace document guards on a
46
+ * workspace id for the same reason and the stakes here are higher: this
47
+ * document creates TENANTS, and running a staging file against a production
48
+ * reseller credential provisions real customers. Optional, because a document
49
+ * meant to be portable across two platform credentials cannot name one —
50
+ * and checked on `plan` as well as `apply`, because a plan is what somebody
51
+ * reads before they approve.
52
+ *
53
+ * It is the PREFIX and not the whole token: the prefix is published on every
54
+ * `whoami` and on every key row, it is not a credential, and a document in
55
+ * git must not contain one. `${env:…}` interpolation works here as it does
56
+ * everywhere else if somebody wants it out of the file entirely.
57
+ */
58
+ import { addressOf } from '../wac/resource-def.js';
59
+ import type { DocumentSchema } from '../wac/document.js';
60
+ export { addressOf };
61
+ /** The default filenames, in the order `--file`-less runs look for them. */
62
+ export declare const PLATFORM_DEFAULT_FILES: string[];
63
+ /** The platform document: six resource types, a `cp_` credential. */
64
+ export declare const PLATFORM_SCHEMA: DocumentSchema;
65
+ //# sourceMappingURL=platform-document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-document.d.ts","sourceRoot":"","sources":["../../src/pac/platform-document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAQH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,oBAAoB,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,UAIlC,CAAC;AAiFF,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,cAO7B,CAAC"}