@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.
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +40 -3
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/deliveries.d.ts +18 -0
- package/dist/commands/deliveries.d.ts.map +1 -0
- package/dist/commands/deliveries.js +46 -0
- package/dist/commands/deliveries.js.map +1 -0
- package/dist/commands/init.d.ts +29 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +176 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/integrations.d.ts +27 -0
- package/dist/commands/integrations.d.ts.map +1 -1
- package/dist/commands/integrations.js +145 -4
- package/dist/commands/integrations.js.map +1 -1
- package/dist/commands/platform-as-code.d.ts +54 -0
- package/dist/commands/platform-as-code.d.ts.map +1 -0
- package/dist/commands/platform-as-code.js +185 -0
- package/dist/commands/platform-as-code.js.map +1 -0
- package/dist/commands/workspace-as-code.d.ts +52 -11
- package/dist/commands/workspace-as-code.d.ts.map +1 -1
- package/dist/commands/workspace-as-code.js +70 -189
- package/dist/commands/workspace-as-code.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +36 -1
- package/dist/help.js.map +1 -1
- package/dist/init/shared.d.ts +61 -0
- package/dist/init/shared.d.ts.map +1 -0
- package/dist/init/shared.js +110 -0
- package/dist/init/shared.js.map +1 -0
- package/dist/init/templates.d.ts +29 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +362 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/pac/platform-document.d.ts +65 -0
- package/dist/pac/platform-document.d.ts.map +1 -0
- package/dist/pac/platform-document.js +149 -0
- package/dist/pac/platform-document.js.map +1 -0
- package/dist/pac/platform-live.d.ts +65 -0
- package/dist/pac/platform-live.d.ts.map +1 -0
- package/dist/pac/platform-live.js +263 -0
- package/dist/pac/platform-live.js.map +1 -0
- package/dist/pac/platform-resources.d.ts +141 -0
- package/dist/pac/platform-resources.d.ts.map +1 -0
- package/dist/pac/platform-resources.js +509 -0
- package/dist/pac/platform-resources.js.map +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +26 -2
- package/dist/run.js.map +1 -1
- package/dist/wac/apply.d.ts.map +1 -1
- package/dist/wac/apply.js +56 -16
- package/dist/wac/apply.js.map +1 -1
- package/dist/wac/diff-warnings.d.ts +35 -0
- package/dist/wac/diff-warnings.d.ts.map +1 -0
- package/dist/wac/diff-warnings.js +88 -0
- package/dist/wac/diff-warnings.js.map +1 -0
- package/dist/wac/diff.d.ts +40 -70
- package/dist/wac/diff.d.ts.map +1 -1
- package/dist/wac/diff.js +114 -91
- package/dist/wac/diff.js.map +1 -1
- package/dist/wac/document-key.d.ts +30 -0
- package/dist/wac/document-key.d.ts.map +1 -0
- package/dist/wac/document-key.js +87 -0
- package/dist/wac/document-key.js.map +1 -0
- package/dist/wac/document.d.ts +75 -65
- package/dist/wac/document.d.ts.map +1 -1
- package/dist/wac/document.js +60 -46
- package/dist/wac/document.js.map +1 -1
- package/dist/wac/execute.d.ts +25 -0
- package/dist/wac/execute.d.ts.map +1 -0
- package/dist/wac/execute.js +126 -0
- package/dist/wac/execute.js.map +1 -0
- package/dist/wac/load.d.ts +17 -0
- package/dist/wac/load.d.ts.map +1 -0
- package/dist/wac/load.js +92 -0
- package/dist/wac/load.js.map +1 -0
- package/dist/wac/resource-def.d.ts +89 -1
- package/dist/wac/resource-def.d.ts.map +1 -1
- package/dist/wac/resource-def.js +4 -0
- package/dist/wac/resource-def.js.map +1 -1
- package/dist/wac/resources.d.ts +9 -11
- package/dist/wac/resources.d.ts.map +1 -1
- package/dist/wac/resources.js +93 -27
- package/dist/wac/resources.js.map +1 -1
- package/dist/wac/values.d.ts +20 -0
- package/dist/wac/values.d.ts.map +1 -1
- package/dist/wac/values.js +46 -0
- package/dist/wac/values.js.map +1 -1
- 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"}
|