@swiftbrowser/web 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +43 -6
  2. package/app/index.html +11 -0
  3. package/node/dist/node/apps.d.ts +16 -0
  4. package/node/dist/node/apps.d.ts.map +1 -1
  5. package/node/dist/node/apps.js +9 -0
  6. package/node/dist/node/apps.js.map +1 -1
  7. package/node/dist/node/config.d.ts +14 -1
  8. package/node/dist/node/config.d.ts.map +1 -1
  9. package/node/dist/node/config.js +20 -3
  10. package/node/dist/node/config.js.map +1 -1
  11. package/node/dist/node/examples.d.ts +2 -1
  12. package/node/dist/node/examples.d.ts.map +1 -1
  13. package/node/dist/node/examples.js +11 -2
  14. package/node/dist/node/examples.js.map +1 -1
  15. package/node/dist/plugins/app-manifests.d.ts +4 -1
  16. package/node/dist/plugins/app-manifests.d.ts.map +1 -1
  17. package/node/dist/plugins/app-manifests.js +3 -2
  18. package/node/dist/plugins/app-manifests.js.map +1 -1
  19. package/node/dist/plugins/asset-catalogs.d.ts +15 -3
  20. package/node/dist/plugins/asset-catalogs.d.ts.map +1 -1
  21. package/node/dist/plugins/asset-catalogs.js +111 -13
  22. package/node/dist/plugins/asset-catalogs.js.map +1 -1
  23. package/node/dist/plugins/included-sites.d.ts +26 -0
  24. package/node/dist/plugins/included-sites.d.ts.map +1 -0
  25. package/node/dist/plugins/included-sites.js +70 -0
  26. package/node/dist/plugins/included-sites.js.map +1 -0
  27. package/node/dist/plugins/pdfImages.d.ts +27 -0
  28. package/node/dist/plugins/pdfImages.d.ts.map +1 -0
  29. package/node/dist/plugins/pdfImages.js +145 -0
  30. package/node/dist/plugins/pdfImages.js.map +1 -0
  31. package/node/dist/plugins/swift-wasm.d.ts +19 -1
  32. package/node/dist/plugins/swift-wasm.d.ts.map +1 -1
  33. package/node/dist/plugins/swift-wasm.js +45 -7
  34. package/node/dist/plugins/swift-wasm.js.map +1 -1
  35. package/node/dist/plugins/symbolTemplates.d.ts +22 -0
  36. package/node/dist/plugins/symbolTemplates.d.ts.map +1 -0
  37. package/node/dist/plugins/symbolTemplates.js +83 -0
  38. package/node/dist/plugins/symbolTemplates.js.map +1 -0
  39. package/package.json +4 -2
  40. package/plugins/app-manifests.ts +3 -2
  41. package/plugins/asset-catalogs.ts +119 -14
  42. package/plugins/included-sites.ts +74 -0
  43. package/plugins/pdfImages.ts +163 -0
  44. package/plugins/swift-wasm.ts +52 -6
  45. package/plugins/symbolTemplates.ts +88 -0
  46. package/public/_headers +7 -0
  47. package/public/app/oauth-callback.html +62 -0
  48. package/src/animation.ts +235 -8
  49. package/src/appFiles.ts +201 -0
  50. package/src/base64.ts +43 -0
  51. package/src/bitmapPlan.ts +143 -0
  52. package/src/bitmapStore.ts +206 -0
  53. package/src/bitmapWorker.ts +41 -0
  54. package/src/bitmapWorkerProtocol.ts +15 -0
  55. package/src/bitmaps.ts +283 -0
  56. package/src/blockedHosts.ts +29 -0
  57. package/src/canvasDrawing.ts +361 -0
  58. package/src/chartDom.ts +2 -2
  59. package/src/chromeColor.ts +179 -0
  60. package/src/clipboard.ts +78 -0
  61. package/src/colorMultiply.ts +93 -0
  62. package/src/colorPicker.ts +44 -0
  63. package/src/corsProxy.ts +45 -0
  64. package/src/gaugeRing.ts +74 -0
  65. package/src/gif.ts +297 -0
  66. package/src/hostRequests.ts +426 -0
  67. package/src/imageOps.ts +592 -0
  68. package/src/interaction.ts +18 -1
  69. package/src/landing.ts +2 -2
  70. package/src/layout/chart.ts +9 -3
  71. package/src/layout/engine.ts +279 -25
  72. package/src/layout/index.ts +3 -1
  73. package/src/layout/text.ts +217 -1
  74. package/src/layout/types.ts +31 -0
  75. package/src/layout/typography.ts +24 -7
  76. package/src/layout/video.ts +24 -0
  77. package/src/layoutDom.ts +53 -3
  78. package/src/main.ts +124 -4
  79. package/src/media.ts +132 -0
  80. package/src/mock.ts +2 -1
  81. package/src/notifications.ts +109 -0
  82. package/src/pathShape.ts +156 -0
  83. package/src/protocol.ts +761 -12
  84. package/src/renderer.ts +584 -36
  85. package/src/richText.ts +65 -0
  86. package/src/scanner.ts +234 -0
  87. package/src/scrollTo.ts +123 -0
  88. package/src/styles.css +212 -7
  89. package/src/styles.ts +31 -0
  90. package/src/swiftData.ts +128 -0
  91. package/src/symbols.ts +564 -21
  92. package/src/translation.ts +126 -0
  93. package/src/video.ts +299 -0
  94. package/src/vite-env.d.ts +6 -0
  95. package/src/wasm.ts +227 -32
  96. package/src/webAuth.ts +267 -0
  97. package/src/webSocket.ts +148 -0
  98. package/src/zoneinfo.ts +269 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * PDF image sets of an asset catalog (Xcode keeps vector artwork as PDF).
3
+ * Browsers cannot show a PDF in an `<img>`, so the build renders each one to
4
+ * a PNG at 3× (the scale of the iPhone the page stands for) with pdf.js and
5
+ * Node's canvas (`@napi-rs/canvas`, which pdf.js installs where it has a
6
+ * prebuilt binary); the page shows the PNG. The manifest's size is the PDF
7
+ * page's in points, read from its MediaBox without rendering.
8
+ *
9
+ * Rendered PNGs are kept in the OS temporary directory, keyed by the PDF's
10
+ * bytes, so a rebuild renders only what changed.
11
+ */
12
+
13
+ import { createHash } from 'node:crypto';
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { deflateSync, inflateSync } from 'node:zlib';
18
+
19
+ /** The scale PDFs are rendered at: points × 3 pixels. */
20
+ export const PDF_RENDER_SCALE = 3;
21
+
22
+ /** A page box (`/MediaBox [x0 y0 x1 y1]`) in some PDF text. */
23
+ const MEDIA_BOX = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/;
24
+
25
+ /**
26
+ * The size in points of a PDF's first page box: its MediaBox, found in the
27
+ * file as written or, when the page objects are compressed (object
28
+ * streams), in its Flate streams; null when there is none.
29
+ */
30
+ export function pdfSize(data: Buffer): { width: number; height: number } | null {
31
+ if (data.toString('latin1', 0, 5) !== '%PDF-') return null;
32
+ const text = data.toString('latin1');
33
+ const fromBox = (match: RegExpExecArray | null) => {
34
+ if (!match) return null;
35
+ const [x0, y0, x1, y1] = match.slice(1, 5).map(Number) as [number, number, number, number];
36
+ const width = Math.abs(x1 - x0);
37
+ const height = Math.abs(y1 - y0);
38
+ return width > 0 && height > 0 ? { width, height } : null;
39
+ };
40
+ const direct = fromBox(MEDIA_BOX.exec(text));
41
+ if (direct) return direct;
42
+ const stream = /stream\r?\n/g;
43
+ for (let match = stream.exec(text); match; match = stream.exec(text)) {
44
+ const start = match.index + match[0].length;
45
+ const end = text.indexOf('endstream', start);
46
+ if (end < 0) break;
47
+ try {
48
+ const size = fromBox(MEDIA_BOX.exec(inflateSync(data.subarray(start, end)).toString('latin1')));
49
+ if (size) return size;
50
+ } catch {
51
+ // Not a Flate stream (an image, say).
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function cacheDir(): string {
58
+ const dir = path.join(os.tmpdir(), 'swiftbrowser-pdf-images');
59
+ mkdirSync(dir, { recursive: true });
60
+ return dir;
61
+ }
62
+
63
+ interface PdfJs {
64
+ getDocument(options: Record<string, unknown>): { promise: Promise<PdfDocument> };
65
+ }
66
+ interface PdfDocument {
67
+ getPage(index: number): Promise<PdfPage>;
68
+ canvasFactory: { create(width: number, height: number): { canvas: unknown; context: CanvasContext } };
69
+ destroy(): Promise<void>;
70
+ }
71
+ interface CanvasContext {
72
+ getImageData(x: number, y: number, width: number, height: number): { data: Uint8ClampedArray; width: number; height: number };
73
+ }
74
+ interface PdfPage {
75
+ getViewport(options: { scale: number }): { width: number; height: number };
76
+ render(options: Record<string, unknown>): { promise: Promise<void> };
77
+ }
78
+
79
+ let pdfjs: Promise<PdfJs> | null = null;
80
+
81
+ /** pdf.js for Node (its legacy build), loaded on the first PDF. */
82
+ function loadPdfJs(): Promise<PdfJs> {
83
+ pdfjs ??= import('pdfjs-dist/legacy/build/pdf.mjs').then((module) => module as unknown as PdfJs);
84
+ return pdfjs;
85
+ }
86
+
87
+ /** Renders queued one at a time: pdf.js shares one worker, and memory stays flat. */
88
+ let queue: Promise<unknown> = Promise.resolve();
89
+
90
+ /** The first page of the PDF at `file` as a PNG at {@link PDF_RENDER_SCALE}× (cached). */
91
+ export function renderPdfImage(file: string): Promise<Buffer> {
92
+ const data = readFileSync(file);
93
+ const key = createHash('sha256').update(data).update(`@${PDF_RENDER_SCALE}x/rgba`).digest('hex').slice(0, 32);
94
+ const cached = path.join(cacheDir(), `${key}.png`);
95
+ if (existsSync(cached)) return Promise.resolve(readFileSync(cached));
96
+ const render = queue.then(async () => {
97
+ const { getDocument } = await loadPdfJs();
98
+ const doc = await getDocument({ data: new Uint8Array(data), isEvalSupported: false, disableFontFace: true, verbosity: 0 }).promise;
99
+ try {
100
+ const page = await doc.getPage(1);
101
+ const viewport = page.getViewport({ scale: PDF_RENDER_SCALE });
102
+ const width = Math.ceil(viewport.width);
103
+ const height = Math.ceil(viewport.height);
104
+ const { canvas, context } = doc.canvasFactory.create(width, height);
105
+ await page.render({ canvasContext: context, canvas, viewport, background: 'rgba(0, 0, 0, 0)' }).promise;
106
+ // Encoded from the pixels: the canvas's own PNG encoder writes
107
+ // transparent pixels as opaque white.
108
+ const png = encodePng(width, height, context.getImageData(0, 0, width, height).data);
109
+ writeFileSync(cached, png);
110
+ return png;
111
+ } finally {
112
+ await doc.destroy();
113
+ }
114
+ });
115
+ queue = render.catch(() => undefined);
116
+ return render;
117
+ }
118
+
119
+ const CRC_TABLE = (() => {
120
+ const table = new Uint32Array(256);
121
+ for (let n = 0; n < 256; n++) {
122
+ let c = n;
123
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
124
+ table[n] = c >>> 0;
125
+ }
126
+ return table;
127
+ })();
128
+
129
+ function crc32(data: Buffer): number {
130
+ let crc = 0xffffffff;
131
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
132
+ return (crc ^ 0xffffffff) >>> 0;
133
+ }
134
+
135
+ function chunk(type: string, data: Buffer): Buffer {
136
+ const head = Buffer.alloc(8);
137
+ head.writeUInt32BE(data.length, 0);
138
+ head.write(type, 4, 'latin1');
139
+ const crc = Buffer.alloc(4);
140
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
141
+ return Buffer.concat([head, data, crc]);
142
+ }
143
+
144
+ /** An 8-bit RGBA PNG of `rgba` (unpremultiplied, row by row). */
145
+ export function encodePng(width: number, height: number, rgba: Uint8ClampedArray): Buffer {
146
+ const header = Buffer.alloc(13);
147
+ header.writeUInt32BE(width, 0);
148
+ header.writeUInt32BE(height, 4);
149
+ header[8] = 8; // bit depth
150
+ header[9] = 6; // RGBA
151
+ const stride = width * 4;
152
+ const rows = Buffer.alloc((stride + 1) * height);
153
+ for (let y = 0; y < height; y++) {
154
+ // Filter type 0 (none) on each row.
155
+ rows.set(rgba.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
156
+ }
157
+ return Buffer.concat([
158
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
159
+ chunk('IHDR', header),
160
+ chunk('IDAT', deflateSync(rows, { level: 9 })),
161
+ chunk('IEND', Buffer.alloc(0)),
162
+ ]);
163
+ }
@@ -19,15 +19,60 @@
19
19
  * `vite build`: copies each spec's `wasm` into the output directory as
20
20
  * `<name>.wasm` (building it first when it is missing and the spec can), so
21
21
  * nothing has to ride along in `public/`. Files that are there anyway are
22
- * still copied by Vite as before.
22
+ * still copied by Vite as before. A module larger than `MAX_FILE_BYTES` (hosts
23
+ * cap a file's size: 25 MiB on Cloudflare Pages) is written as parts,
24
+ * `<name>.wasm.0`, `.1`…, with a manifest `<name>.wasm.parts.json`
25
+ * (`{ size, parts }`); the page joins them (src/wasm.ts).
23
26
  */
24
27
 
25
- import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
28
+ import { closeSync, copyFileSync, createReadStream, existsSync, mkdirSync, openSync, readSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
26
29
  import path from 'node:path';
27
30
  import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite';
28
31
  import type { AppSpec } from '../node/apps.js';
29
32
  import type { BuildEndPayload, BuildErrorPayload, BuildStartPayload } from '../src/devEvents.js';
30
33
 
34
+ /** The largest file a site holds: under Cloudflare Pages' 25 MiB, with room to spare. */
35
+ export const MAX_FILE_BYTES = 24 * 1024 * 1024;
36
+
37
+ /** The manifest of a module written as parts (`<name>.wasm.parts.json`). */
38
+ export interface WasmParts {
39
+ /** The module's size in bytes. */
40
+ size: number;
41
+ /** The parts' file names, in order, next to the manifest. */
42
+ parts: string[];
43
+ }
44
+
45
+ /**
46
+ * Writes `source` to `outDir` as `<name>.wasm`, or, when larger than
47
+ * `maxBytes`, as parts of at most `maxBytes` each plus their manifest.
48
+ * Returns the files written.
49
+ */
50
+ export function writeModule(source: string, outDir: string, name: string, maxBytes = MAX_FILE_BYTES): string[] {
51
+ const dest = path.join(outDir, `${name}.wasm`);
52
+ const size = statSync(source).size;
53
+ if (size <= maxBytes) {
54
+ if (path.resolve(source) !== path.resolve(dest)) copyFileSync(source, dest);
55
+ return [dest];
56
+ }
57
+ rmSync(dest, { force: true });
58
+ const parts: string[] = [];
59
+ const fd = openSync(source, 'r');
60
+ try {
61
+ const buffer = Buffer.alloc(maxBytes);
62
+ for (let offset = 0, index = 0; offset < size; offset += maxBytes, index++) {
63
+ const length = readSync(fd, buffer, 0, Math.min(maxBytes, size - offset), offset);
64
+ const part = `${name}.wasm.${index}`;
65
+ writeFileSync(path.join(outDir, part), buffer.subarray(0, length));
66
+ parts.push(part);
67
+ }
68
+ } finally {
69
+ closeSync(fd);
70
+ }
71
+ const manifest: WasmParts = { size, parts };
72
+ writeFileSync(path.join(outDir, `${name}.wasm.parts.json`), JSON.stringify(manifest) + '\n');
73
+ return [...parts.map((part) => path.join(outDir, part)), path.join(outDir, `${name}.wasm.parts.json`)];
74
+ }
75
+
31
76
  export interface SwiftWasmOptions {
32
77
  /** Debounce window for bursts of file changes, in ms. */
33
78
  debounceMs?: number;
@@ -190,14 +235,15 @@ export function swiftWasm(specs: readonly AppSpec[], options: SwiftWasmOptions =
190
235
  continue;
191
236
  }
192
237
  mkdirSync(outDir, { recursive: true });
193
- const dest = path.join(outDir, `${spec.name}.wasm`);
194
- if (path.resolve(spec.wasm) !== path.resolve(dest)) copyFileSync(spec.wasm, dest);
238
+ const written = writeModule(spec.wasm, outDir, spec.name);
239
+ if (written.length > 1) log(`${spec.name}: ${written.length - 1} parts of at most ${MAX_FILE_BYTES} bytes (the host's file size limit)`);
195
240
  }
196
241
  // Modules of apps outside the registry (the examples' `public/*.wasm`,
197
242
  // copied with the public dir) are not part of this site.
198
- const known = new Set(specs.map((spec) => `${spec.name}.wasm`));
243
+ const known = new Set(specs.map((spec) => spec.name));
199
244
  for (const entry of readdirSync(outDir)) {
200
- if (entry.endsWith('.wasm') && !known.has(entry)) rmSync(path.join(outDir, entry), { force: true });
245
+ const module = /^([A-Za-z0-9_-]+)\.wasm(?:\.\d+|\.parts\.json)?$/.exec(entry);
246
+ if (module && !known.has(module[1]!)) rmSync(path.join(outDir, entry), { force: true });
201
247
  }
202
248
  };
203
249
 
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Custom symbols (`.symbolset`) are SF Symbols templates: one SVG sheet
3
+ * holding notes, guides and the glyph in every weight and scale
4
+ * (`<g id="Regular-M">`, `<g id="Bold-L">`…). Drawn as a mask, the whole
5
+ * sheet shows as a gray smear. The page needs one glyph, so the build
6
+ * publishes the Regular-M variant (else another Regular one) on its own: its
7
+ * group, in a view box from the template's left to right margin lines for it
8
+ * (`left-margin-Regular-M`; older templates have one plain `left-margin`
9
+ * pair, Regular-M's) and the height of those lines, which is the symbol's box.
10
+ *
11
+ * An SVG that is not a template is published as it is.
12
+ */
13
+
14
+ /** The variants tried, in order. */
15
+ const VARIANTS = ['Regular-M', 'Regular-S', 'Regular-L', 'Medium-M', 'Semibold-M', 'Light-M'];
16
+
17
+ export interface SymbolGlyph {
18
+ /** A standalone SVG of the glyph. */
19
+ svg: string;
20
+ /** The glyph's box in template units (100 per point at the template's point size). */
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ /** The glyph of an SF Symbols template, or null when `svg` is not one. */
26
+ export function symbolTemplateGlyph(svg: string): SymbolGlyph | null {
27
+ for (const variant of VARIANTS) {
28
+ const glyph = extractVariant(svg, variant);
29
+ if (glyph) return glyph;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function extractVariant(svg: string, variant: string): SymbolGlyph | null {
35
+ const open = new RegExp(`<g\\s[^>]*id="${variant}"[^>]*>`).exec(svg);
36
+ if (!open) return null;
37
+ const transform = /transform="([^"]*)"/.exec(open[0])?.[1];
38
+ const inner = groupContents(svg, open.index + open[0].length);
39
+ // Older templates have one pair of margins, `left-margin` and `right-margin`, for Regular-M.
40
+ const plain = variant === 'Regular-M';
41
+ const left = marginLine(svg, `left-margin-${variant}`) ?? (plain ? marginLine(svg, 'left-margin') : null);
42
+ const right = marginLine(svg, `right-margin-${variant}`) ?? (plain ? marginLine(svg, 'right-margin') : null);
43
+ if (inner === null || !left || !right) return null;
44
+ const x = left.x;
45
+ const y = Math.min(left.y1, left.y2);
46
+ const width = right.x - left.x;
47
+ const height = Math.abs(left.y2 - left.y1);
48
+ if (!(width > 0 && height > 0)) return null;
49
+ // The template's classes style its preview; a mask needs only the shapes.
50
+ const shapes = inner.replace(/\sclass="[^"]*"/g, '').trim();
51
+ const n = (v: number) => String(Math.round(v * 1000) / 1000);
52
+ const box = `${n(x)} ${n(y)} ${n(width)} ${n(height)}`;
53
+ const group = transform ? `<g transform="${transform}">${shapes}</g>` : `<g>${shapes}</g>`;
54
+ return {
55
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${box}" width="${n(width)}" height="${n(height)}">${group}</svg>\n`,
56
+ width,
57
+ height,
58
+ };
59
+ }
60
+
61
+ /** What lies between `start` (just after a `<g …>`) and its matching `</g>`. */
62
+ function groupContents(svg: string, start: number): string | null {
63
+ const tag = /<g[\s>]|<\/g>|<g\/>/g;
64
+ tag.lastIndex = start;
65
+ let depth = 1;
66
+ for (let match = tag.exec(svg); match; match = tag.exec(svg)) {
67
+ if (match[0] === '</g>') depth--;
68
+ else if (match[0] !== '<g/>' && !isSelfClosing(svg, match.index)) depth++;
69
+ if (depth === 0) return svg.slice(start, match.index);
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function isSelfClosing(svg: string, index: number): boolean {
75
+ const end = svg.indexOf('>', index);
76
+ return end > 0 && svg[end - 1] === '/';
77
+ }
78
+
79
+ /** A margin `<line>`'s x and vertical extent. */
80
+ function marginLine(svg: string, id: string): { x: number; y1: number; y2: number } | null {
81
+ const line = new RegExp(`<line\\s[^>]*id="${id}"[^>]*>`).exec(svg)?.[0];
82
+ if (!line) return null;
83
+ const attr = (name: string) => Number(new RegExp(`\\s${name}="(-?[\\d.]+)"`).exec(line)?.[1]);
84
+ const x = attr('x1');
85
+ const y1 = attr('y1');
86
+ const y2 = attr('y2');
87
+ return [x, y1, y2].every(Number.isFinite) ? { x, y1, y2 } : null;
88
+ }
package/public/_headers CHANGED
@@ -5,6 +5,13 @@
5
5
  Cache-Control: no-cache
6
6
  Content-Type: application/wasm
7
7
 
8
+ # A module over the host's file size limit is published as parts and a
9
+ # manifest (plugins/swift-wasm.ts); not content-hashed either.
10
+ /*.wasm.parts.json
11
+ Cache-Control: no-cache
12
+ /*.wasm.*
13
+ Cache-Control: no-cache
14
+
8
15
  # Vite's hashed bundles can be cached forever.
9
16
  /assets/*
10
17
  Cache-Control: public, max-age=31536000, immutable
@@ -0,0 +1,62 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <meta name="referrer" content="no-referrer" />
8
+ <title>Signing in…</title>
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ font: 17px/1.4 -apple-system, system-ui, sans-serif;
17
+ background: Canvas;
18
+ color: CanvasText;
19
+ }
20
+ p {
21
+ margin: 16px;
22
+ text-align: center;
23
+ opacity: 0.7;
24
+ }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <p id="status">Signing in…</p>
29
+ <script>
30
+ // The end of a sign-in (docs/ops-protocol.md, "Signing in"). The app's
31
+ // OAuth redirect (`icecubesapp://`) was swapped for this page, so the
32
+ // provider sent the sign-in window here, with `code` (or `error`) in the
33
+ // query. Hand the URL to the app's page (src/webAuth.ts) and close: by
34
+ // postMessage to the opener, and on a BroadcastChannel for when a
35
+ // provider's Cross-Origin-Opener-Policy cut the window off from it.
36
+ (function () {
37
+ var message = { type: 'sb-web-auth-callback', url: location.href };
38
+ var delivered = false;
39
+ try {
40
+ if (window.opener && !window.opener.closed) {
41
+ window.opener.postMessage(message, location.origin);
42
+ delivered = true;
43
+ }
44
+ } catch (error) {}
45
+ try {
46
+ var channel = new BroadcastChannel('sb-web-auth');
47
+ channel.postMessage(message);
48
+ channel.close();
49
+ delivered = true;
50
+ } catch (error) {}
51
+ // The code is single-use and the app has it now; keep it out of history.
52
+ try {
53
+ history.replaceState(null, '', location.pathname);
54
+ } catch (error) {}
55
+ document.getElementById('status').textContent = delivered
56
+ ? 'You can close this window.'
57
+ : 'Return to the app to finish signing in.';
58
+ if (delivered) setTimeout(function () { window.close(); }, 50);
59
+ })();
60
+ </script>
61
+ </body>
62
+ </html>