@nowline/embed 0.2.5 → 0.4.1
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/auth/allowlist.d.ts +20 -0
- package/dist/auth/allowlist.d.ts.map +1 -0
- package/dist/auth/allowlist.js +33 -0
- package/dist/auth/allowlist.js.map +1 -0
- package/dist/auth/env.d.ts +23 -0
- package/dist/auth/env.d.ts.map +1 -0
- package/dist/auth/env.js +24 -0
- package/dist/auth/env.js.map +1 -0
- package/dist/auth/firebase-auth.client.d.ts +21 -0
- package/dist/auth/firebase-auth.client.d.ts.map +1 -0
- package/dist/auth/firebase-auth.client.js +142 -0
- package/dist/auth/firebase-auth.client.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/meta.json +578 -411
- package/dist/nowline.esm.js +952 -222
- package/dist/nowline.esm.js.map +4 -4
- package/dist/nowline.min.js +78 -77
- package/dist/nowline.min.js.map +4 -4
- package/dist/pipeline.d.ts +3 -3
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +40 -69
- package/dist/pipeline.js.map +1 -1
- package/package.json +17 -10
- package/src/auth/allowlist.ts +32 -0
- package/src/auth/env.ts +37 -0
- package/src/auth/firebase-auth.client.ts +174 -0
- package/src/index.ts +42 -0
- package/src/pipeline.ts +52 -89
- package/dist/no-op-include-resolver.d.ts +0 -4
- package/dist/no-op-include-resolver.d.ts.map +0 -1
- package/dist/no-op-include-resolver.js +0 -18
- package/dist/no-op-include-resolver.js.map +0 -1
- package/src/no-op-include-resolver.ts +0 -22
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import {
|
|
1
|
+
import { type ParseResult } from '@nowline/browser';
|
|
2
|
+
import type { ThemeName } from '@nowline/layout';
|
|
3
3
|
export interface EmbedRenderOptions {
|
|
4
4
|
theme?: ThemeName;
|
|
5
5
|
today?: Date;
|
|
@@ -14,7 +14,7 @@ export interface EmbedRenderOptions {
|
|
|
14
14
|
idPrefix?: string;
|
|
15
15
|
}
|
|
16
16
|
export interface EmbedParseResult {
|
|
17
|
-
ast:
|
|
17
|
+
ast: ParseResult['ast'];
|
|
18
18
|
/** Lexer + parser + Langium validation diagnostics, normalized to strings. */
|
|
19
19
|
errors: string[];
|
|
20
20
|
}
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAcA,OAAO,EAMH,KAAK,WAAW,EACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,WAAW,kBAAkB;IAC/B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAID,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU3E;AAED,wBAAsB,YAAY,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,kBAAuB,GACjC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aAGnB,OAAO,EAAE,MAAM,EAAE;gBADjC,OAAO,EAAE,MAAM,EACC,OAAO,EAAE,MAAM,EAAE;CAKxC;AAKD,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
|
package/dist/pipeline.js
CHANGED
|
@@ -1,79 +1,51 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// Thin shim around `@nowline/browser`'s pipeline. The browser package
|
|
2
|
+
// owns parse / resolveIncludes / layout / render; the embed layers two
|
|
3
|
+
// embed-specific behaviours on top:
|
|
4
|
+
//
|
|
5
|
+
// - Throws `EmbedRenderError` on failure instead of returning a
|
|
6
|
+
// discriminated union. The Mermaid-compatible `nowline.render(source)`
|
|
7
|
+
// surface promises a string; throwing matches the documented v1
|
|
8
|
+
// contract and keeps the auto-scan path's per-block error handling
|
|
9
|
+
// simple.
|
|
10
|
+
// - Latches a once-per-page-load `console.warn` the first time an
|
|
11
|
+
// `include` directive is encountered. The browser pipeline emits a
|
|
12
|
+
// structured callback for each skip; the embed converts that into a
|
|
13
|
+
// single, deduped user-visible message.
|
|
14
|
+
import { __resetBrowserPipelineForTests, parseSource as browserParseSource, renderSource as browserRenderSource, } from '@nowline/browser';
|
|
15
|
+
const EMBED_SOURCE_PATH = '/embed.nowline';
|
|
14
16
|
let includeWarningEmitted = false;
|
|
15
|
-
function getServices() {
|
|
16
|
-
if (!cachedServices)
|
|
17
|
-
cachedServices = createNowlineServices();
|
|
18
|
-
return cachedServices;
|
|
19
|
-
}
|
|
20
|
-
function freshUri() {
|
|
21
|
-
return URI.parse(`memory:///nowline-embed-${++docCounter}.nowline`);
|
|
22
|
-
}
|
|
23
17
|
export async function parseSource(source) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (const d of doc.diagnostics ?? []) {
|
|
34
|
-
if (d.severity === 1)
|
|
35
|
-
errors.push(d.message);
|
|
36
|
-
}
|
|
37
|
-
return { ast: doc.parseResult.value, errors };
|
|
18
|
+
const { ast, diagnostics } = await browserParseSource(source, {
|
|
19
|
+
filePath: EMBED_SOURCE_PATH,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
ast,
|
|
23
|
+
errors: diagnostics
|
|
24
|
+
.filter((d) => d.severity === 'error')
|
|
25
|
+
.map((d) => d.message),
|
|
26
|
+
};
|
|
38
27
|
}
|
|
39
28
|
export async function renderSource(source, options = {}) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
throw new EmbedRenderError(`Failed to parse Nowline source: ${parsed.errors.join('; ')}`, parsed.errors);
|
|
43
|
-
}
|
|
44
|
-
const services = getServices();
|
|
45
|
-
const resolved = await resolveIncludes(parsed.ast, '/embed.nowline', {
|
|
46
|
-
services: services.Nowline,
|
|
47
|
-
readFile: noOpIncludeReadFile,
|
|
48
|
-
});
|
|
49
|
-
let sawIncludeWarning = false;
|
|
50
|
-
const blockingErrors = [];
|
|
51
|
-
for (const diag of resolved.diagnostics) {
|
|
52
|
-
if (diag.severity !== 'error')
|
|
53
|
-
continue;
|
|
54
|
-
if (isNoOpIncludeDiagnosticMessage(diag.message)) {
|
|
55
|
-
sawIncludeWarning = true;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
blockingErrors.push(diag.message);
|
|
59
|
-
}
|
|
60
|
-
if (blockingErrors.length > 0) {
|
|
61
|
-
throw new EmbedRenderError(`Failed to resolve Nowline source: ${blockingErrors.join('; ')}`, blockingErrors);
|
|
62
|
-
}
|
|
63
|
-
if (sawIncludeWarning && !includeWarningEmitted) {
|
|
64
|
-
includeWarningEmitted = true;
|
|
65
|
-
console.warn('nowline: `include` directives are skipped in the browser embed (single-file mode). ' +
|
|
66
|
-
'Render multi-file roadmaps with the CLI or the GitHub Action.');
|
|
67
|
-
}
|
|
68
|
-
const model = layoutRoadmap(parsed.ast, resolved, {
|
|
29
|
+
const browserOptions = {
|
|
30
|
+
filePath: EMBED_SOURCE_PATH,
|
|
69
31
|
theme: options.theme,
|
|
70
32
|
today: options.today,
|
|
71
33
|
locale: options.locale,
|
|
72
34
|
width: options.width,
|
|
73
|
-
});
|
|
74
|
-
return renderSvg(model, {
|
|
75
35
|
idPrefix: options.idPrefix,
|
|
76
|
-
|
|
36
|
+
onSkippedInclude: () => {
|
|
37
|
+
if (!includeWarningEmitted) {
|
|
38
|
+
includeWarningEmitted = true;
|
|
39
|
+
console.warn('nowline: `include` directives are skipped in the browser embed (single-file mode). ' +
|
|
40
|
+
'Render multi-file roadmaps with the CLI or the GitHub Action.');
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const result = await browserRenderSource(source, browserOptions);
|
|
45
|
+
if (result.kind === 'svg')
|
|
46
|
+
return result.svg;
|
|
47
|
+
const messages = result.diagnostics.filter((d) => d.severity === 'error').map((d) => d.message);
|
|
48
|
+
throw new EmbedRenderError(`Failed to render Nowline source: ${messages.join('; ')}`, messages);
|
|
77
49
|
}
|
|
78
50
|
export class EmbedRenderError extends Error {
|
|
79
51
|
details;
|
|
@@ -88,7 +60,6 @@ export class EmbedRenderError extends Error {
|
|
|
88
60
|
// reset the latch between cases.
|
|
89
61
|
export function __resetEmbedPipelineForTests() {
|
|
90
62
|
includeWarningEmitted = false;
|
|
91
|
-
|
|
92
|
-
docCounter = 0;
|
|
63
|
+
__resetBrowserPipelineForTests();
|
|
93
64
|
}
|
|
94
65
|
//# sourceMappingURL=pipeline.js.map
|
package/dist/pipeline.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,uEAAuE;AACvE,oCAAoC;AACpC,EAAE;AACF,iEAAiE;AACjE,0EAA0E;AAC1E,mEAAmE;AACnE,sEAAsE;AACtE,aAAa;AACb,mEAAmE;AACnE,sEAAsE;AACtE,uEAAuE;AACvE,2CAA2C;AAE3C,OAAO,EACH,8BAA8B,EAE9B,WAAW,IAAI,kBAAkB,EACjC,YAAY,IAAI,mBAAmB,GAGtC,MAAM,kBAAkB,CAAC;AAG1B,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAsB3C,IAAI,qBAAqB,GAAG,KAAK,CAAC;AAElC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC5C,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE;QAC1D,QAAQ,EAAE,iBAAiB;KAC9B,CAAC,CAAC;IACH,OAAO;QACH,GAAG;QACH,MAAM,EAAE,WAAW;aACd,MAAM,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC;aACpD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;KAC7B,CAAC;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,MAAc,EACd,UAA8B,EAAE;IAEhC,MAAM,cAAc,GAAyB;QACzC,QAAQ,EAAE,iBAAiB;QAC3B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,gBAAgB,EAAE,GAAG,EAAE;YACnB,IAAI,CAAC,qBAAqB,EAAE,CAAC;gBACzB,qBAAqB,GAAG,IAAI,CAAC;gBAC7B,OAAO,CAAC,IAAI,CACR,qFAAqF;oBACjF,+DAA+D,CACtE,CAAC;YACN,CAAC;QACL,CAAC;KACJ,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAE7C,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAChG,MAAM,IAAI,gBAAgB,CAAC,oCAAoC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;AACpG,CAAC;AAED,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAGnB;IAFpB,YACI,OAAe,EACC,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACnC,CAAC;CACJ;AAED,uEAAuE;AACvE,wEAAwE;AACxE,iCAAiC;AACjC,MAAM,UAAU,4BAA4B;IACxC,qBAAqB,GAAG,KAAK,CAAC;IAC9B,8BAA8B,EAAE,CAAC;AACrC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowline/embed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Browser embed bundle for Nowline. Drop a <script> tag and ```nowline``` blocks render in place.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22",
|
|
8
|
+
"pnpm": ">=11"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"sideEffects": false,
|
|
8
12
|
"main": "./dist/index.js",
|
|
@@ -35,23 +39,26 @@
|
|
|
35
39
|
"cdn"
|
|
36
40
|
],
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"langium": "~4.2.
|
|
39
|
-
"@nowline/
|
|
40
|
-
"@nowline/
|
|
41
|
-
"@nowline/
|
|
42
|
+
"langium": "~4.2.4",
|
|
43
|
+
"@nowline/browser": "0.4.1",
|
|
44
|
+
"@nowline/core": "0.4.1",
|
|
45
|
+
"@nowline/renderer": "0.4.1",
|
|
46
|
+
"@nowline/layout": "0.4.1"
|
|
42
47
|
},
|
|
43
48
|
"devDependencies": {
|
|
44
|
-
"@types/node": "^
|
|
45
|
-
"esbuild": "^0.
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
+
"@types/node": "^25.9.1",
|
|
50
|
+
"esbuild": "^0.28.0",
|
|
51
|
+
"firebase": "^12.13.0",
|
|
52
|
+
"happy-dom": "^20.9.0",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"vitest": "^4.1.7"
|
|
49
55
|
},
|
|
50
56
|
"scripts": {
|
|
51
57
|
"build": "tsc -b tsconfig.json && node scripts/bundle.mjs",
|
|
52
58
|
"watch": "tsc -b tsconfig.json --watch",
|
|
53
59
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
54
60
|
"bundle": "node scripts/bundle.mjs",
|
|
61
|
+
"bundle:dev": "node scripts/bundle.mjs --dev",
|
|
55
62
|
"check-size": "node scripts/check-size.mjs",
|
|
56
63
|
"test": "vitest run",
|
|
57
64
|
"test:watch": "vitest"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist for the dev embed bundle's Firebase Auth gate.
|
|
3
|
+
*
|
|
4
|
+
* An email is allowlisted if either:
|
|
5
|
+
* 1. Its domain is in ALLOWED_DOMAINS (e.g. anything @nowline.io), OR
|
|
6
|
+
* 2. The exact lowercase email is in ALLOWED_EMAILS.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the commercial site's `auth-allowlist.ts` so both Lolay
|
|
9
|
+
* dev surfaces (the marketing site and the embed CDN dev tier) share
|
|
10
|
+
* one allowlist policy. Keep this short; when it grows past ~5 entries,
|
|
11
|
+
* migrate to Firebase custom claims (Admin SDK) or a Firestore
|
|
12
|
+
* `allowlist` collection.
|
|
13
|
+
*
|
|
14
|
+
* See specs/embed.md § Bootstrap status (dev auth gate) and
|
|
15
|
+
* the infrastructure deploy runbook § 4 for the deploy-side wiring.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const ALLOWED_DOMAINS: readonly string[] = ['nowline.io'];
|
|
19
|
+
|
|
20
|
+
export const ALLOWED_EMAILS: readonly string[] = [
|
|
21
|
+
// Add additional allowlisted Google account emails here, one per line.
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function isAllowlisted(email: string | null | undefined): boolean {
|
|
25
|
+
if (!email) return false;
|
|
26
|
+
const normalized = email.trim().toLowerCase();
|
|
27
|
+
if (ALLOWED_EMAILS.includes(normalized)) return true;
|
|
28
|
+
const at = normalized.lastIndexOf('@');
|
|
29
|
+
if (at === -1) return false;
|
|
30
|
+
const domain = normalized.slice(at + 1);
|
|
31
|
+
return ALLOWED_DOMAINS.includes(domain);
|
|
32
|
+
}
|
package/src/auth/env.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time environment helpers for `@nowline/embed`.
|
|
3
|
+
*
|
|
4
|
+
* `__NOWLINE_EMBED_ENV__` is substituted at bundle time by esbuild's
|
|
5
|
+
* `define` (see `packages/embed/scripts/bundle.mjs`). The substitution
|
|
6
|
+
* lets the prod minified bundle dead-code-eliminate the dev auth gate
|
|
7
|
+
* and its Firebase imports — when the constant folds to the literal
|
|
8
|
+
* string `"prod"`, every `IS_DEV` branch becomes `false` and esbuild's
|
|
9
|
+
* minifier strips the dynamic-import site that pulls in `firebase/app`
|
|
10
|
+
* + `firebase/auth`.
|
|
11
|
+
*
|
|
12
|
+
* The `typeof` guards keep this module safe to import under vitest,
|
|
13
|
+
* where esbuild's define never runs and the identifier is undeclared.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
declare const __NOWLINE_EMBED_ENV__: string;
|
|
17
|
+
declare const __NOWLINE_EMBED_VERSION__: string;
|
|
18
|
+
declare const __NOWLINE_EMBED_SHA__: string;
|
|
19
|
+
|
|
20
|
+
export type EmbedEnv = 'dev' | 'prod';
|
|
21
|
+
|
|
22
|
+
export const EMBED_ENV: EmbedEnv =
|
|
23
|
+
typeof __NOWLINE_EMBED_ENV__ !== 'undefined' && __NOWLINE_EMBED_ENV__ === 'dev'
|
|
24
|
+
? 'dev'
|
|
25
|
+
: 'prod';
|
|
26
|
+
|
|
27
|
+
export const IS_DEV: boolean = EMBED_ENV === 'dev';
|
|
28
|
+
export const IS_PROD: boolean = EMBED_ENV === 'prod';
|
|
29
|
+
|
|
30
|
+
export const EMBED_VERSION: string =
|
|
31
|
+
typeof __NOWLINE_EMBED_VERSION__ !== 'undefined' ? __NOWLINE_EMBED_VERSION__ : '0.0.0';
|
|
32
|
+
|
|
33
|
+
export const EMBED_SHA: string =
|
|
34
|
+
typeof __NOWLINE_EMBED_SHA__ !== 'undefined' ? __NOWLINE_EMBED_SHA__ : 'unknown';
|
|
35
|
+
|
|
36
|
+
export const PROD_ORIGIN = 'https://embed.nowline.io';
|
|
37
|
+
export const DEV_ORIGIN = 'https://embed.nowline.dev';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side Firebase Auth gate for the dev embed bundle.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the commercial site's `firebase-auth.client.ts` so the
|
|
5
|
+
* two Lolay dev surfaces share one allowlist UX. Loaded only on the
|
|
6
|
+
* dev build (when `__NOWLINE_EMBED_ENV__ === 'dev'`); the prod build
|
|
7
|
+
* tree-shakes the dynamic import in `src/index.ts` and never pulls
|
|
8
|
+
* `firebase/app` or `firebase/auth` into the IIFE.
|
|
9
|
+
*
|
|
10
|
+
* Renders a full-viewport overlay with z-index 2147483647 so it sits
|
|
11
|
+
* above any host-page content. Until the visitor signs in with an
|
|
12
|
+
* allowlisted Google account, the overlay stays put; once allowlisted,
|
|
13
|
+
* it removes itself and the embed's auto-scan reaches the rendered
|
|
14
|
+
* SVG underneath. The host page is told nothing — the gate is purely
|
|
15
|
+
* client-side, opaque to the embedder.
|
|
16
|
+
*
|
|
17
|
+
* See specs/embed.md § Bootstrap status (dev auth gate) and
|
|
18
|
+
* the infrastructure deploy runbook § 4 for the deploy-side wiring.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type FirebaseApp, initializeApp } from 'firebase/app';
|
|
22
|
+
import {
|
|
23
|
+
type Auth,
|
|
24
|
+
GoogleAuthProvider,
|
|
25
|
+
getAuth,
|
|
26
|
+
onAuthStateChanged,
|
|
27
|
+
signInWithPopup,
|
|
28
|
+
signOut,
|
|
29
|
+
type User,
|
|
30
|
+
} from 'firebase/auth';
|
|
31
|
+
import { isAllowlisted } from './allowlist.js';
|
|
32
|
+
|
|
33
|
+
// esbuild-substituted at build time from PUBLIC_FIREBASE_* env vars in
|
|
34
|
+
// .github/workflows/embed-cdn.yml (sourced from the `embed-dev` GitHub
|
|
35
|
+
// environment-scoped variables — see the infrastructure deploy runbook § 2.5).
|
|
36
|
+
declare const __NOWLINE_FIREBASE_API_KEY__: string;
|
|
37
|
+
declare const __NOWLINE_FIREBASE_AUTH_DOMAIN__: string;
|
|
38
|
+
declare const __NOWLINE_FIREBASE_PROJECT_ID__: string;
|
|
39
|
+
declare const __NOWLINE_FIREBASE_APP_ID__: string;
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
apiKey: typeof __NOWLINE_FIREBASE_API_KEY__ !== 'undefined' ? __NOWLINE_FIREBASE_API_KEY__ : '',
|
|
43
|
+
authDomain:
|
|
44
|
+
typeof __NOWLINE_FIREBASE_AUTH_DOMAIN__ !== 'undefined'
|
|
45
|
+
? __NOWLINE_FIREBASE_AUTH_DOMAIN__
|
|
46
|
+
: '',
|
|
47
|
+
projectId:
|
|
48
|
+
typeof __NOWLINE_FIREBASE_PROJECT_ID__ !== 'undefined'
|
|
49
|
+
? __NOWLINE_FIREBASE_PROJECT_ID__
|
|
50
|
+
: '',
|
|
51
|
+
appId: typeof __NOWLINE_FIREBASE_APP_ID__ !== 'undefined' ? __NOWLINE_FIREBASE_APP_ID__ : '',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const OVERLAY_ID = 'nowline-embed-dev-auth-overlay';
|
|
55
|
+
|
|
56
|
+
let app: FirebaseApp | null = null;
|
|
57
|
+
let auth: Auth | null = null;
|
|
58
|
+
|
|
59
|
+
function getOrCreateOverlay(): HTMLDivElement {
|
|
60
|
+
let overlay = document.getElementById(OVERLAY_ID) as HTMLDivElement | null;
|
|
61
|
+
if (overlay) return overlay;
|
|
62
|
+
|
|
63
|
+
overlay = document.createElement('div');
|
|
64
|
+
overlay.id = OVERLAY_ID;
|
|
65
|
+
overlay.setAttribute('role', 'dialog');
|
|
66
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
67
|
+
overlay.setAttribute('aria-label', 'Internal preview sign-in');
|
|
68
|
+
overlay.style.cssText = [
|
|
69
|
+
'position: fixed',
|
|
70
|
+
'inset: 0',
|
|
71
|
+
'z-index: 2147483647',
|
|
72
|
+
'background-color: #ffffff',
|
|
73
|
+
'color: #1a1a2e',
|
|
74
|
+
'display: flex',
|
|
75
|
+
'align-items: center',
|
|
76
|
+
'justify-content: center',
|
|
77
|
+
'padding: 1.5rem',
|
|
78
|
+
'font-family: system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
79
|
+
].join(';');
|
|
80
|
+
document.body.appendChild(overlay);
|
|
81
|
+
return overlay;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeOverlay(): void {
|
|
85
|
+
const overlay = document.getElementById(OVERLAY_ID);
|
|
86
|
+
overlay?.remove();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderSignIn(overlay: HTMLDivElement, onClick: () => void): void {
|
|
90
|
+
overlay.innerHTML = `
|
|
91
|
+
<div style="max-width: 28rem; text-align: center;">
|
|
92
|
+
<div style="font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #5a5a6a; margin-bottom: 0.75rem;">embed.nowline.dev — internal preview</div>
|
|
93
|
+
<h1 style="font-size: 1.875rem; font-weight: 700; margin: 0 0 0.75rem;">Sign in to continue</h1>
|
|
94
|
+
<p style="margin: 0 0 1.5rem; color: #5a5a6a;">Access is limited to allowlisted Lolay accounts. Production embed is at <a href="https://embed.nowline.io" style="color: #1a4ed8;">embed.nowline.io</a>.</p>
|
|
95
|
+
<button type="button" id="nowline-embed-dev-signin-btn" style="display: inline-block; padding: 0.75rem 1.5rem; border-radius: 8px; background-color: #e53e3e; color: #ffffff; font-weight: 700; border: 1px solid transparent; cursor: pointer; font-size: 1rem;">Sign in with Google</button>
|
|
96
|
+
</div>
|
|
97
|
+
`;
|
|
98
|
+
const btn = overlay.querySelector<HTMLButtonElement>('#nowline-embed-dev-signin-btn');
|
|
99
|
+
btn?.addEventListener('click', onClick);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderDenied(overlay: HTMLDivElement, email: string, onSignOut: () => void): void {
|
|
103
|
+
overlay.innerHTML = `
|
|
104
|
+
<div style="max-width: 28rem; text-align: center;">
|
|
105
|
+
<div style="font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #5a5a6a; margin-bottom: 0.75rem;">embed.nowline.dev — internal preview</div>
|
|
106
|
+
<h1 style="font-size: 1.875rem; font-weight: 700; margin: 0 0 0.75rem;">Access denied</h1>
|
|
107
|
+
<p style="margin: 0 0 1.5rem; color: #5a5a6a;">${escapeHtml(email)} is not on the allowlist for this preview environment. Production embed is publicly available at <a href="https://embed.nowline.io" style="color: #1a4ed8;">embed.nowline.io</a>.</p>
|
|
108
|
+
<button type="button" id="nowline-embed-dev-signout-btn" style="display: inline-block; padding: 0.75rem 1.5rem; border-radius: 8px; background-color: transparent; color: #1a1a2e; font-weight: 700; border: 1px solid #c0c0c0; cursor: pointer; font-size: 1rem;">Sign out</button>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
const btn = overlay.querySelector<HTMLButtonElement>('#nowline-embed-dev-signout-btn');
|
|
112
|
+
btn?.addEventListener('click', onSignOut);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function escapeHtml(value: string): string {
|
|
116
|
+
return value
|
|
117
|
+
.replace(/&/g, '&')
|
|
118
|
+
.replace(/</g, '<')
|
|
119
|
+
.replace(/>/g, '>')
|
|
120
|
+
.replace(/"/g, '"')
|
|
121
|
+
.replace(/'/g, ''');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function startDevAuthGate(): void {
|
|
125
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
126
|
+
if (!config.apiKey || !config.authDomain || !config.projectId || !config.appId) {
|
|
127
|
+
// No firebase config baked in. Likely a local `pnpm bundle` without
|
|
128
|
+
// PUBLIC_FIREBASE_* exported (CI deploys always set them). Skip the
|
|
129
|
+
// overlay rather than render an unrecoverable dialog so devs aren't
|
|
130
|
+
// locked out of their own local builds.
|
|
131
|
+
console.warn(
|
|
132
|
+
'[nowline-embed-dev-auth-gate] Missing PUBLIC_FIREBASE_* env vars at build time; gate is disabled. Configure them in .github/workflows/embed-cdn.yml or your local environment to enable.',
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create the overlay up front so it covers content while Firebase
|
|
138
|
+
// initialises; subsequent renders call getOrCreateOverlay() again
|
|
139
|
+
// to find the same element.
|
|
140
|
+
getOrCreateOverlay();
|
|
141
|
+
|
|
142
|
+
app ??= initializeApp(config);
|
|
143
|
+
auth ??= getAuth(app);
|
|
144
|
+
|
|
145
|
+
const provider = new GoogleAuthProvider();
|
|
146
|
+
|
|
147
|
+
const handleSignIn = async (): Promise<void> => {
|
|
148
|
+
try {
|
|
149
|
+
await signInWithPopup(auth as Auth, provider);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[nowline-embed-dev-auth-gate] Sign-in failed:', err);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleSignOut = async (): Promise<void> => {
|
|
156
|
+
try {
|
|
157
|
+
await signOut(auth as Auth);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('[nowline-embed-dev-auth-gate] Sign-out failed:', err);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
onAuthStateChanged(auth as Auth, (user: User | null) => {
|
|
164
|
+
if (!user) {
|
|
165
|
+
renderSignIn(getOrCreateOverlay(), handleSignIn);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isAllowlisted(user.email)) {
|
|
169
|
+
renderDenied(getOrCreateOverlay(), user.email ?? 'unknown', handleSignOut);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
removeOverlay();
|
|
173
|
+
});
|
|
174
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
// The IIFE bundle exposes everything below as `window.nowline.*`. ESM
|
|
6
6
|
// consumers import named exports from the package root.
|
|
7
7
|
|
|
8
|
+
import { EMBED_SHA, EMBED_VERSION } from './auth/env.js';
|
|
9
|
+
|
|
10
|
+
// Build-time constant substituted by esbuild's `define` (see
|
|
11
|
+
// `scripts/bundle.mjs`). Reading it directly at the call site (rather
|
|
12
|
+
// than via an `IS_DEV` re-export from env.ts) means esbuild can fold
|
|
13
|
+
// the `if` below to a literal `true`/`false` at minify time and
|
|
14
|
+
// dead-code-eliminate the dynamic-import branch in the prod bundle,
|
|
15
|
+
// stripping `firebase/app` + `firebase/auth` from `dist/nowline.min.js`.
|
|
16
|
+
// The `typeof` guard keeps the file safe to import under vitest, where
|
|
17
|
+
// esbuild's define never runs and the identifier is undeclared.
|
|
18
|
+
declare const __NOWLINE_EMBED_ENV__: string;
|
|
19
|
+
|
|
8
20
|
import {
|
|
9
21
|
__resetAutoScanForTests,
|
|
10
22
|
type AutoScanInputs,
|
|
@@ -23,6 +35,18 @@ import { type EmbedTheme, effectiveTheme, resolveSystemTheme } from './theme.js'
|
|
|
23
35
|
|
|
24
36
|
export { type AutoScanResult, type EmbedParseResult, EmbedRenderError, type EmbedTheme };
|
|
25
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Bundle provenance, mirroring the legal-comment banner that
|
|
40
|
+
* `scripts/bundle.mjs` injects at the top of every artifact:
|
|
41
|
+
* `@nowline/embed <version> sha=<short-sha> built=<iso-utc>`.
|
|
42
|
+
*
|
|
43
|
+
* Exposed on the IIFE global as `nowline.version` / `nowline.sha` so
|
|
44
|
+
* pages and bug reports can identify the exact build without scraping
|
|
45
|
+
* the comment banner.
|
|
46
|
+
*/
|
|
47
|
+
export const version: string = EMBED_VERSION;
|
|
48
|
+
export const sha: string = EMBED_SHA;
|
|
49
|
+
|
|
26
50
|
const DEFAULT_SELECTOR = 'pre code.language-nowline, code.language-nowline';
|
|
27
51
|
|
|
28
52
|
export interface InitializeOptions {
|
|
@@ -149,6 +173,24 @@ export const run = init;
|
|
|
149
173
|
// the auto-scan branch.
|
|
150
174
|
if (typeof document !== 'undefined' && !autoStartScheduled) {
|
|
151
175
|
autoStartScheduled = true;
|
|
176
|
+
|
|
177
|
+
// Dev-only Firebase Auth gate (embed.nowline.dev). The condition
|
|
178
|
+
// reads `__NOWLINE_EMBED_ENV__` directly so esbuild's `define`
|
|
179
|
+
// substitutes a literal string at minify time — the prod bundle
|
|
180
|
+
// sees `if ("prod" === "dev")` → `if (false)` and DCEs the dynamic
|
|
181
|
+
// import that pulls in `firebase/app` + `firebase/auth`.
|
|
182
|
+
// `check-size.mjs` asserts no `firebase` literal survives in the
|
|
183
|
+
// prod IIFE as a belt-and-suspenders check.
|
|
184
|
+
if (typeof __NOWLINE_EMBED_ENV__ !== 'undefined' && __NOWLINE_EMBED_ENV__ === 'dev') {
|
|
185
|
+
void import('./auth/firebase-auth.client.js')
|
|
186
|
+
.then(({ startDevAuthGate }) => {
|
|
187
|
+
startDevAuthGate();
|
|
188
|
+
})
|
|
189
|
+
.catch((err) => {
|
|
190
|
+
console.error('[nowline] dev auth gate failed to load:', err);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
152
194
|
const start = (): void => {
|
|
153
195
|
if (!config.startOnLoad) return;
|
|
154
196
|
// Fire-and-forget; render errors are surfaced via `console.error`
|