@pwfabric/cli 0.0.0-stage → 1.0.0-rc.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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +122 -2
  3. package/dist/args.d.ts +11 -0
  4. package/dist/args.js +43 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/bin.d.ts +2 -0
  7. package/dist/bin.js +10 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/commands/add.d.ts +3 -0
  10. package/dist/commands/add.js +56 -0
  11. package/dist/commands/add.js.map +1 -0
  12. package/dist/commands/build.d.ts +3 -0
  13. package/dist/commands/build.js +55 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/create.d.ts +9 -0
  16. package/dist/commands/create.js +119 -0
  17. package/dist/commands/create.js.map +1 -0
  18. package/dist/commands/dev.d.ts +12 -0
  19. package/dist/commands/dev.js +107 -0
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/lib/flags.d.ts +10 -0
  22. package/dist/commands/lib/flags.js +33 -0
  23. package/dist/commands/lib/flags.js.map +1 -0
  24. package/dist/commands/lib/identity.d.ts +38 -0
  25. package/dist/commands/lib/identity.js +120 -0
  26. package/dist/commands/lib/identity.js.map +1 -0
  27. package/dist/commands/lib/project.d.ts +34 -0
  28. package/dist/commands/lib/project.js +78 -0
  29. package/dist/commands/lib/project.js.map +1 -0
  30. package/dist/commands/lib/report.d.ts +15 -0
  31. package/dist/commands/lib/report.js +75 -0
  32. package/dist/commands/lib/report.js.map +1 -0
  33. package/dist/commands/lib/starter.d.ts +41 -0
  34. package/dist/commands/lib/starter.js +136 -0
  35. package/dist/commands/lib/starter.js.map +1 -0
  36. package/dist/commands/lib/static-server.d.ts +45 -0
  37. package/dist/commands/lib/static-server.js +176 -0
  38. package/dist/commands/lib/static-server.js.map +1 -0
  39. package/dist/commands/pack.d.ts +5 -0
  40. package/dist/commands/pack.js +56 -0
  41. package/dist/commands/pack.js.map +1 -0
  42. package/dist/commands/registry.d.ts +7 -0
  43. package/dist/commands/registry.js +16 -0
  44. package/dist/commands/registry.js.map +1 -0
  45. package/dist/commands/run.d.ts +14 -0
  46. package/dist/commands/run.js +79 -0
  47. package/dist/commands/run.js.map +1 -0
  48. package/dist/commands/types.d.ts +34 -0
  49. package/dist/commands/types.js +10 -0
  50. package/dist/commands/types.js.map +1 -0
  51. package/dist/commands/validate.d.ts +6 -0
  52. package/dist/commands/validate.js +33 -0
  53. package/dist/commands/validate.js.map +1 -0
  54. package/dist/dev/assemble.d.ts +47 -0
  55. package/dist/dev/assemble.js +339 -0
  56. package/dist/dev/assemble.js.map +1 -0
  57. package/dist/dev/compile.d.ts +54 -0
  58. package/dist/dev/compile.js +215 -0
  59. package/dist/dev/compile.js.map +1 -0
  60. package/dist/dev/csp.d.ts +67 -0
  61. package/dist/dev/csp.js +260 -0
  62. package/dist/dev/csp.js.map +1 -0
  63. package/dist/dev/diagnostics.d.ts +21 -0
  64. package/dist/dev/diagnostics.js +22 -0
  65. package/dist/dev/diagnostics.js.map +1 -0
  66. package/dist/dev/envelope.d.ts +92 -0
  67. package/dist/dev/envelope.js +154 -0
  68. package/dist/dev/envelope.js.map +1 -0
  69. package/dist/dev/media.d.ts +53 -0
  70. package/dist/dev/media.js +128 -0
  71. package/dist/dev/media.js.map +1 -0
  72. package/dist/dev/needs-host.d.ts +2 -0
  73. package/dist/dev/needs-host.js +32 -0
  74. package/dist/dev/needs-host.js.map +1 -0
  75. package/dist/dev/output.d.ts +9 -0
  76. package/dist/dev/output.js +32 -0
  77. package/dist/dev/output.js.map +1 -0
  78. package/dist/dev/overlay.d.ts +21 -0
  79. package/dist/dev/overlay.js +59 -0
  80. package/dist/dev/overlay.js.map +1 -0
  81. package/dist/dev/server.d.ts +27 -0
  82. package/dist/dev/server.js +268 -0
  83. package/dist/dev/server.js.map +1 -0
  84. package/dist/dev/shell.d.ts +94 -0
  85. package/dist/dev/shell.js +267 -0
  86. package/dist/dev/shell.js.map +1 -0
  87. package/dist/dev/sources.d.ts +45 -0
  88. package/dist/dev/sources.js +165 -0
  89. package/dist/dev/sources.js.map +1 -0
  90. package/dist/dev/theme.d.ts +51 -0
  91. package/dist/dev/theme.js +185 -0
  92. package/dist/dev/theme.js.map +1 -0
  93. package/dist/dev/types.d.ts +40 -0
  94. package/dist/dev/types.js +11 -0
  95. package/dist/dev/types.js.map +1 -0
  96. package/dist/dev/watcher.d.ts +29 -0
  97. package/dist/dev/watcher.js +150 -0
  98. package/dist/dev/watcher.js.map +1 -0
  99. package/dist/host/definitions.d.ts +61 -0
  100. package/dist/host/definitions.js +136 -0
  101. package/dist/host/definitions.js.map +1 -0
  102. package/dist/host/http.d.ts +39 -0
  103. package/dist/host/http.js +156 -0
  104. package/dist/host/http.js.map +1 -0
  105. package/dist/host/local-host.d.ts +59 -0
  106. package/dist/host/local-host.js +420 -0
  107. package/dist/host/local-host.js.map +1 -0
  108. package/dist/host/oidc.d.ts +46 -0
  109. package/dist/host/oidc.js +192 -0
  110. package/dist/host/oidc.js.map +1 -0
  111. package/dist/host/query.d.ts +35 -0
  112. package/dist/host/query.js +158 -0
  113. package/dist/host/query.js.map +1 -0
  114. package/dist/host/receipts.d.ts +25 -0
  115. package/dist/host/receipts.js +72 -0
  116. package/dist/host/receipts.js.map +1 -0
  117. package/dist/host/records.d.ts +73 -0
  118. package/dist/host/records.js +229 -0
  119. package/dist/host/records.js.map +1 -0
  120. package/dist/host/tokens.d.ts +28 -0
  121. package/dist/host/tokens.js +52 -0
  122. package/dist/host/tokens.js.map +1 -0
  123. package/dist/host/types.d.ts +32 -0
  124. package/dist/host/types.js +11 -0
  125. package/dist/host/types.js.map +1 -0
  126. package/dist/index.d.ts +18 -0
  127. package/dist/index.js +51 -0
  128. package/dist/index.js.map +1 -0
  129. package/package.json +66 -3
@@ -0,0 +1,136 @@
1
+ /**
2
+ * What the local host reads from the pwpack folder.
3
+ *
4
+ * app/definitions/auth.json who may enter (definitions.md, `auth.json`)
5
+ * app/definitions/entities.json the collections the host keeps
6
+ * app/surfaces/<key>.receipt.json the pages it serves
7
+ * app/data/<typeId>.jsonl | .json seed rows (a convention of this host)
8
+ *
9
+ * Read on every call, never cached: `pwf dev` edits these files while the
10
+ * host runs, and a definition change must apply without a restart. An absent
11
+ * file is "not declared"; an unreadable one is reported as an error rather
12
+ * than guessed around.
13
+ */
14
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ function readJson(path) {
17
+ if (!existsSync(path))
18
+ return undefined;
19
+ const text = readFileSync(path, 'utf8');
20
+ try {
21
+ return JSON.parse(text);
22
+ }
23
+ catch (err) {
24
+ throw new Error(`${path} is not valid JSON: ${err.message}`);
25
+ }
26
+ }
27
+ const isObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
28
+ const strings = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []);
29
+ /** `undefined` when the app declares no `auth.json` (or an empty one). */
30
+ export function readAuth(root) {
31
+ const raw = readJson(join(root, 'app', 'definitions', 'auth.json'));
32
+ if (!isObject(raw) || Object.keys(raw).length === 0)
33
+ return undefined;
34
+ const topology = raw.topology === 'external' ? 'external' : 'app';
35
+ const iss = isObject(raw.issuer) ? raw.issuer : undefined;
36
+ const access = isObject(raw.access) ? raw.access : {};
37
+ const rules = (Array.isArray(access.rules) ? access.rules : [])
38
+ .filter(isObject)
39
+ .map((r) => ({ subject: String(r.subject ?? ''), model: String(r.model ?? ''), rule: String(r.rule ?? '') }));
40
+ return {
41
+ required: raw.required === true,
42
+ topology,
43
+ methods: strings(raw.methods),
44
+ ...(iss && typeof iss.issuer === 'string' && typeof iss.clientId === 'string'
45
+ ? {
46
+ issuer: {
47
+ issuer: iss.issuer,
48
+ clientId: iss.clientId,
49
+ scopes: Array.isArray(iss.scopes) ? strings(iss.scopes) : ['openid', 'profile', 'email'],
50
+ ...(typeof iss.audience === 'string' && iss.audience ? { audience: iss.audience } : {}),
51
+ },
52
+ }
53
+ : {}),
54
+ access: { rules },
55
+ };
56
+ }
57
+ export function readEntities(root) {
58
+ const raw = readJson(join(root, 'app', 'definitions', 'entities.json'));
59
+ const list = isObject(raw) && Array.isArray(raw.entities) ? raw.entities : [];
60
+ return list.filter(isObject).flatMap((e) => {
61
+ if (typeof e.typeId !== 'string' || !e.typeId)
62
+ return [];
63
+ const fields = (Array.isArray(e.fields) ? e.fields : []).filter(isObject).flatMap((f) => typeof f.name === 'string' && f.name
64
+ ? [{ name: f.name, type: typeof f.type === 'string' ? f.type : 'text', required: f.required === true }]
65
+ : []);
66
+ return [
67
+ {
68
+ typeId: e.typeId,
69
+ ...(typeof e.displayName === 'string' ? { displayName: e.displayName } : {}),
70
+ fields,
71
+ ...(typeof e.scope === 'string' ? { scope: e.scope } : {}),
72
+ },
73
+ ];
74
+ });
75
+ }
76
+ /** A page key is a file name, never a path. */
77
+ export const PAGE_KEY = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
78
+ export function surfaceKeys(root) {
79
+ const dir = join(root, 'app', 'surfaces');
80
+ if (!existsSync(dir))
81
+ return [];
82
+ return readdirSync(dir)
83
+ .filter((f) => f.endsWith('.receipt.json'))
84
+ .map((f) => f.slice(0, -'.receipt.json'.length))
85
+ .filter((k) => PAGE_KEY.test(k))
86
+ .sort();
87
+ }
88
+ export function readReceipt(root, key) {
89
+ if (!PAGE_KEY.test(key))
90
+ return undefined;
91
+ const raw = readJson(join(root, 'app', 'surfaces', `${key}.receipt.json`));
92
+ return isObject(raw) ? raw : undefined;
93
+ }
94
+ /**
95
+ * Seed rows for one collection.
96
+ *
97
+ * THE CONVENTION (proposed for the spec): `app/data/<typeId>.json` holding
98
+ * `{ "data": [ …records ] }` — the same `data` envelope a record list answer
99
+ * uses. A record may carry its own `id`; `createdAt`/`updatedAt` are filled
100
+ * when absent. Also accepted, for hand-written seed files only:
101
+ * `app/data/<typeId>.jsonl`, one JSON object per line (blank lines skipped),
102
+ * read when no `.json` file exists.
103
+ */
104
+ export function readSeed(root, typeId) {
105
+ if (!PAGE_KEY.test(typeId))
106
+ return [];
107
+ const dir = join(root, 'app', 'data');
108
+ const jsonPath = join(dir, `${typeId}.json`);
109
+ const json = readJson(jsonPath);
110
+ if (json !== undefined) {
111
+ if (!isObject(json) || !Array.isArray(json.data)) {
112
+ throw new Error(`${jsonPath} must be { "data": [ ...records ] }`);
113
+ }
114
+ return json.data.filter(isObject);
115
+ }
116
+ const jsonl = join(dir, `${typeId}.jsonl`);
117
+ if (!existsSync(jsonl))
118
+ return [];
119
+ return readFileSync(jsonl, 'utf8')
120
+ .split(/\r?\n/)
121
+ .map((line, i) => [line.trim(), i + 1])
122
+ .filter(([line]) => line !== '')
123
+ .map(([line, n]) => {
124
+ let row;
125
+ try {
126
+ row = JSON.parse(line);
127
+ }
128
+ catch {
129
+ throw new Error(`${jsonl}:${n} is not valid JSON`);
130
+ }
131
+ if (!isObject(row))
132
+ throw new Error(`${jsonl}:${n} is not a JSON object`);
133
+ return row;
134
+ });
135
+ }
136
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/host/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AA+BhC,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IACvC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACvC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAA;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,uBAAwB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAA;IACzE,CAAC;AACH,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAgC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AACvH,MAAM,OAAO,GAAG,CAAC,CAAU,EAAY,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AAEvH,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;IACnE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACrE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAA;IACjE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA;IACrD,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5D,MAAM,CAAC,QAAQ,CAAC;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC/G,OAAO;QACL,QAAQ,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI;QAC/B,QAAQ;QACR,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;QAC7B,GAAG,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ;YAC3E,CAAC,CAAC;gBACE,MAAM,EAAE;oBACN,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;oBACtB,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;oBACxF,GAAG,CAAC,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACxF;aACF;YACH,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,EAAE,EAAE,KAAK,EAAE;KAClB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC,CAAA;IACvE,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7E,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACzC,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM;YAAE,OAAO,EAAE,CAAA;QACxD,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CACtF,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI;YAClC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YACvG,CAAC,CAAC,EAAE,CACP,CAAA;QACD,OAAO;YACL;gBACE,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,GAAG,CAAC,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5E,MAAM;gBACN,GAAG,CAAC,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3D;SACF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,+CAA+C;AAC/C,MAAM,CAAC,MAAM,QAAQ,GAAG,6BAA6B,CAAA;AAErD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;IACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC/B,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;SAC1C,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;SAC/C,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;SAC/B,IAAI,EAAE,CAAA;AACX,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,GAAW;IACnD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,GAAG,eAAe,CAAC,CAAC,CAAA;IAC1E,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AACxC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAc;IACnD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAA;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,OAAO,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ,qCAAqC,CAAC,CAAA;QACnE,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACnC,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,QAAQ,CAAC,CAAA;IAC1C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACjC,OAAO,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC;SAC/B,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,CAAU,CAAC;SAC/C,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;SAC/B,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE;QACjB,IAAI,GAAY,CAAA;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,oBAAoB,CAAC,CAAA;QACpD,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,uBAAuB,CAAC,CAAA;QACzE,OAAO,GAAG,CAAA;IACZ,CAAC,CAAC,CAAA;AACN,CAAC","sourcesContent":["/**\n * What the local host reads from the pwpack folder.\n *\n * app/definitions/auth.json who may enter (definitions.md, `auth.json`)\n * app/definitions/entities.json the collections the host keeps\n * app/surfaces/<key>.receipt.json the pages it serves\n * app/data/<typeId>.jsonl | .json seed rows (a convention of this host)\n *\n * Read on every call, never cached: `pwf dev` edits these files while the\n * host runs, and a definition change must apply without a restart. An absent\n * file is \"not declared\"; an unreadable one is reported as an error rather\n * than guessed around.\n */\n\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport interface AuthDeclaration {\n readonly required: boolean\n readonly topology: 'app' | 'external'\n readonly methods: readonly string[]\n readonly issuer?: {\n readonly issuer: string\n readonly clientId: string\n readonly scopes: readonly string[]\n readonly audience?: string\n }\n readonly access: {\n readonly rules: readonly { readonly subject: string; readonly model: string; readonly rule: string }[]\n }\n}\n\nexport interface EntityField {\n readonly name: string\n readonly type: string\n readonly required: boolean\n}\n\nexport interface EntityDeclaration {\n readonly typeId: string\n readonly displayName?: string\n readonly fields: readonly EntityField[]\n /** Carried member (not in the spec yet): `\"owner\"` makes the collection per-visitor. */\n readonly scope?: string\n}\n\nfunction readJson(path: string): unknown {\n if (!existsSync(path)) return undefined\n const text = readFileSync(path, 'utf8')\n try {\n return JSON.parse(text) as unknown\n } catch (err) {\n throw new Error(`${path} is not valid JSON: ${(err as Error).message}`)\n }\n}\n\nconst isObject = (v: unknown): v is Record<string, unknown> => Boolean(v) && typeof v === 'object' && !Array.isArray(v)\nconst strings = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [])\n\n/** `undefined` when the app declares no `auth.json` (or an empty one). */\nexport function readAuth(root: string): AuthDeclaration | undefined {\n const raw = readJson(join(root, 'app', 'definitions', 'auth.json'))\n if (!isObject(raw) || Object.keys(raw).length === 0) return undefined\n const topology = raw.topology === 'external' ? 'external' : 'app'\n const iss = isObject(raw.issuer) ? raw.issuer : undefined\n const access = isObject(raw.access) ? raw.access : {}\n const rules = (Array.isArray(access.rules) ? access.rules : [])\n .filter(isObject)\n .map((r) => ({ subject: String(r.subject ?? ''), model: String(r.model ?? ''), rule: String(r.rule ?? '') }))\n return {\n required: raw.required === true,\n topology,\n methods: strings(raw.methods),\n ...(iss && typeof iss.issuer === 'string' && typeof iss.clientId === 'string'\n ? {\n issuer: {\n issuer: iss.issuer,\n clientId: iss.clientId,\n scopes: Array.isArray(iss.scopes) ? strings(iss.scopes) : ['openid', 'profile', 'email'],\n ...(typeof iss.audience === 'string' && iss.audience ? { audience: iss.audience } : {}),\n },\n }\n : {}),\n access: { rules },\n }\n}\n\nexport function readEntities(root: string): EntityDeclaration[] {\n const raw = readJson(join(root, 'app', 'definitions', 'entities.json'))\n const list = isObject(raw) && Array.isArray(raw.entities) ? raw.entities : []\n return list.filter(isObject).flatMap((e) => {\n if (typeof e.typeId !== 'string' || !e.typeId) return []\n const fields = (Array.isArray(e.fields) ? e.fields : []).filter(isObject).flatMap((f) =>\n typeof f.name === 'string' && f.name\n ? [{ name: f.name, type: typeof f.type === 'string' ? f.type : 'text', required: f.required === true }]\n : []\n )\n return [\n {\n typeId: e.typeId,\n ...(typeof e.displayName === 'string' ? { displayName: e.displayName } : {}),\n fields,\n ...(typeof e.scope === 'string' ? { scope: e.scope } : {}),\n },\n ]\n })\n}\n\n/** A page key is a file name, never a path. */\nexport const PAGE_KEY = /^[A-Za-z0-9][A-Za-z0-9_-]*$/\n\nexport function surfaceKeys(root: string): string[] {\n const dir = join(root, 'app', 'surfaces')\n if (!existsSync(dir)) return []\n return readdirSync(dir)\n .filter((f) => f.endsWith('.receipt.json'))\n .map((f) => f.slice(0, -'.receipt.json'.length))\n .filter((k) => PAGE_KEY.test(k))\n .sort()\n}\n\nexport function readReceipt(root: string, key: string): Record<string, unknown> | undefined {\n if (!PAGE_KEY.test(key)) return undefined\n const raw = readJson(join(root, 'app', 'surfaces', `${key}.receipt.json`))\n return isObject(raw) ? raw : undefined\n}\n\n/**\n * Seed rows for one collection.\n *\n * THE CONVENTION (proposed for the spec): `app/data/<typeId>.json` holding\n * `{ \"data\": [ …records ] }` — the same `data` envelope a record list answer\n * uses. A record may carry its own `id`; `createdAt`/`updatedAt` are filled\n * when absent. Also accepted, for hand-written seed files only:\n * `app/data/<typeId>.jsonl`, one JSON object per line (blank lines skipped),\n * read when no `.json` file exists.\n */\nexport function readSeed(root: string, typeId: string): Record<string, unknown>[] {\n if (!PAGE_KEY.test(typeId)) return []\n const dir = join(root, 'app', 'data')\n const jsonPath = join(dir, `${typeId}.json`)\n const json = readJson(jsonPath)\n if (json !== undefined) {\n if (!isObject(json) || !Array.isArray(json.data)) {\n throw new Error(`${jsonPath} must be { \"data\": [ ...records ] }`)\n }\n return json.data.filter(isObject)\n }\n const jsonl = join(dir, `${typeId}.jsonl`)\n if (!existsSync(jsonl)) return []\n return readFileSync(jsonl, 'utf8')\n .split(/\\r?\\n/)\n .map((line, i) => [line.trim(), i + 1] as const)\n .filter(([line]) => line !== '')\n .map(([line, n]) => {\n let row: unknown\n try {\n row = JSON.parse(line)\n } catch {\n throw new Error(`${jsonl}:${n} is not valid JSON`)\n }\n if (!isObject(row)) throw new Error(`${jsonl}:${n} is not a JSON object`)\n return row\n })\n}\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The few HTTP chores the local host needs, on bare `node:http`.
3
+ *
4
+ * Every answer is JSON (or a redirect). Bodies are read up to 1 MiB and
5
+ * refused past it; JSON routes refuse any other content type. Nothing here
6
+ * logs — a request carries session tokens, and a development server's console
7
+ * is copied into bug reports.
8
+ */
9
+ import type { IncomingMessage, ServerResponse } from 'node:http';
10
+ /** Request bodies past this size are refused with 413. */
11
+ export declare const MAX_BODY_BYTES: number;
12
+ /** A failure that becomes `{ error: { code, message } }` with `status`. */
13
+ export declare class HostError extends Error {
14
+ readonly status: number;
15
+ readonly code: string;
16
+ constructor(status: number, code: string, message: string);
17
+ }
18
+ export declare function sendJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void;
19
+ export declare function sendError(res: ServerResponse, err: HostError): void;
20
+ /** `302` to `location`, with any cookies to set on the way. */
21
+ export declare function redirect(res: ServerResponse, location: string, cookies?: readonly string[]): void;
22
+ /** The raw body, refused past `MAX_BODY_BYTES`. */
23
+ export declare function readBody(req: IncomingMessage): Promise<string>;
24
+ /** A JSON object body. An empty body is `{}`; anything else that is not an object is refused. */
25
+ export declare function readJsonObject(req: IncomingMessage): Promise<Record<string, unknown>>;
26
+ /** A form-encoded body (the OAuth token endpoint's only format). */
27
+ export declare function readForm(req: IncomingMessage): Promise<URLSearchParams>;
28
+ export declare function cookiesOf(req: IncomingMessage): Record<string, string>;
29
+ /**
30
+ * The visitor's credential: `x-app-session`, else `Authorization: Bearer`.
31
+ * The session protocol accepts either on every session-server route.
32
+ */
33
+ export declare function credentialOf(req: IncomingMessage): string | undefined;
34
+ /**
35
+ * Only a path on the app's own origin may be the way back — the same rule the
36
+ * runner applies before it builds a start address (`//host` and `/\host` are
37
+ * other sites to a browser).
38
+ */
39
+ export declare function safeReturnTo(value: string | null): string;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The few HTTP chores the local host needs, on bare `node:http`.
3
+ *
4
+ * Every answer is JSON (or a redirect). Bodies are read up to 1 MiB and
5
+ * refused past it; JSON routes refuse any other content type. Nothing here
6
+ * logs — a request carries session tokens, and a development server's console
7
+ * is copied into bug reports.
8
+ */
9
+ /** Request bodies past this size are refused with 413. */
10
+ export const MAX_BODY_BYTES = 1024 * 1024;
11
+ /** A failure that becomes `{ error: { code, message } }` with `status`. */
12
+ export class HostError extends Error {
13
+ status;
14
+ code;
15
+ constructor(status, code, message) {
16
+ super(message);
17
+ this.status = status;
18
+ this.code = code;
19
+ }
20
+ }
21
+ export function sendJson(res, status, body, headers = {}) {
22
+ const text = JSON.stringify(body);
23
+ res.writeHead(status, {
24
+ 'content-type': 'application/json; charset=utf-8',
25
+ 'cache-control': 'no-store',
26
+ 'content-length': String(Buffer.byteLength(text)),
27
+ ...headers,
28
+ });
29
+ res.end(text);
30
+ }
31
+ export function sendError(res, err) {
32
+ sendJson(res, err.status, { error: { code: err.code, message: err.message } });
33
+ }
34
+ /** `302` to `location`, with any cookies to set on the way. */
35
+ export function redirect(res, location, cookies = []) {
36
+ res.writeHead(302, {
37
+ location,
38
+ 'cache-control': 'no-store',
39
+ ...(cookies.length > 0 ? { 'set-cookie': [...cookies] } : {}),
40
+ });
41
+ res.end();
42
+ }
43
+ /** The raw body, refused past `MAX_BODY_BYTES`. */
44
+ export function readBody(req) {
45
+ const tooLarge = () => new HostError(413, 'PAYLOAD_TOO_LARGE', 'The request body is larger than 1 MiB.');
46
+ const declared = Number(req.headers['content-length']);
47
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
48
+ req.resume();
49
+ return Promise.reject(tooLarge());
50
+ }
51
+ // Listeners, not an async iterator: leaving an iterator early destroys the
52
+ // socket, and the refusal would never reach the client. Past the limit the
53
+ // rest of the body is drained and dropped while the 413 goes out.
54
+ return new Promise((resolve, reject) => {
55
+ const chunks = [];
56
+ let size = 0;
57
+ let refused = false;
58
+ req.on('data', (chunk) => {
59
+ if (refused)
60
+ return;
61
+ const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
62
+ size += buf.length;
63
+ if (size > MAX_BODY_BYTES) {
64
+ refused = true;
65
+ chunks.length = 0;
66
+ reject(tooLarge());
67
+ return;
68
+ }
69
+ chunks.push(buf);
70
+ });
71
+ req.on('end', () => {
72
+ if (!refused)
73
+ resolve(Buffer.concat(chunks).toString('utf8'));
74
+ });
75
+ req.on('error', reject);
76
+ });
77
+ }
78
+ function mediaTypeOf(req) {
79
+ return String(req.headers['content-type'] ?? '')
80
+ .split(';')[0]
81
+ .trim()
82
+ .toLowerCase();
83
+ }
84
+ /** A JSON object body. An empty body is `{}`; anything else that is not an object is refused. */
85
+ export async function readJsonObject(req) {
86
+ const text = await readBody(req);
87
+ if (text.trim() === '')
88
+ return {};
89
+ if (mediaTypeOf(req) !== 'application/json') {
90
+ throw new HostError(415, 'UNSUPPORTED_MEDIA_TYPE', 'Send the body as application/json.');
91
+ }
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(text);
95
+ }
96
+ catch {
97
+ throw new HostError(400, 'INVALID_JSON', 'The request body is not valid JSON.');
98
+ }
99
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
100
+ throw new HostError(400, 'INVALID_BODY', 'The request body must be a JSON object.');
101
+ }
102
+ return parsed;
103
+ }
104
+ /** A form-encoded body (the OAuth token endpoint's only format). */
105
+ export async function readForm(req) {
106
+ const text = await readBody(req);
107
+ if (mediaTypeOf(req) !== 'application/x-www-form-urlencoded') {
108
+ throw new HostError(415, 'UNSUPPORTED_MEDIA_TYPE', 'Send the body as application/x-www-form-urlencoded.');
109
+ }
110
+ return new URLSearchParams(text);
111
+ }
112
+ export function cookiesOf(req) {
113
+ const out = {};
114
+ for (const part of String(req.headers.cookie ?? '').split(';')) {
115
+ const at = part.indexOf('=');
116
+ if (at <= 0)
117
+ continue;
118
+ const name = part.slice(0, at).trim();
119
+ const value = part.slice(at + 1).trim();
120
+ try {
121
+ out[name] = decodeURIComponent(value);
122
+ }
123
+ catch {
124
+ out[name] = value;
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * The visitor's credential: `x-app-session`, else `Authorization: Bearer`.
131
+ * The session protocol accepts either on every session-server route.
132
+ */
133
+ export function credentialOf(req) {
134
+ const session = req.headers['x-app-session'];
135
+ if (typeof session === 'string' && session.trim())
136
+ return session.trim();
137
+ const auth = req.headers.authorization;
138
+ if (typeof auth === 'string') {
139
+ const m = /^Bearer\s+(\S+)\s*$/i.exec(auth);
140
+ if (m)
141
+ return m[1];
142
+ }
143
+ return undefined;
144
+ }
145
+ /**
146
+ * Only a path on the app's own origin may be the way back — the same rule the
147
+ * runner applies before it builds a start address (`//host` and `/\host` are
148
+ * other sites to a browser).
149
+ */
150
+ export function safeReturnTo(value) {
151
+ if (!value || !value.startsWith('/'))
152
+ return '/';
153
+ const second = value.charAt(1);
154
+ return second === '/' || second === '\\' ? '/' : value;
155
+ }
156
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/host/http.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The few HTTP chores the local host needs, on bare `node:http`.\n *\n * Every answer is JSON (or a redirect). Bodies are read up to 1 MiB and\n * refused past it; JSON routes refuse any other content type. Nothing here\n * logs — a request carries session tokens, and a development server's console\n * is copied into bug reports.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\n\n/** Request bodies past this size are refused with 413. */\nexport const MAX_BODY_BYTES = 1024 * 1024\n\n/** A failure that becomes `{ error: { code, message } }` with `status`. */\nexport class HostError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string\n ) {\n super(message)\n }\n}\n\nexport function sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n const text = JSON.stringify(body)\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n 'content-length': String(Buffer.byteLength(text)),\n ...headers,\n })\n res.end(text)\n}\n\nexport function sendError(res: ServerResponse, err: HostError): void {\n sendJson(res, err.status, { error: { code: err.code, message: err.message } })\n}\n\n/** `302` to `location`, with any cookies to set on the way. */\nexport function redirect(res: ServerResponse, location: string, cookies: readonly string[] = []): void {\n res.writeHead(302, {\n location,\n 'cache-control': 'no-store',\n ...(cookies.length > 0 ? { 'set-cookie': [...cookies] } : {}),\n })\n res.end()\n}\n\n/** The raw body, refused past `MAX_BODY_BYTES`. */\nexport function readBody(req: IncomingMessage): Promise<string> {\n const tooLarge = (): HostError => new HostError(413, 'PAYLOAD_TOO_LARGE', 'The request body is larger than 1 MiB.')\n const declared = Number(req.headers['content-length'])\n if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {\n req.resume()\n return Promise.reject(tooLarge())\n }\n // Listeners, not an async iterator: leaving an iterator early destroys the\n // socket, and the refusal would never reach the client. Past the limit the\n // rest of the body is drained and dropped while the 413 goes out.\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n let refused = false\n req.on('data', (chunk: Buffer | string) => {\n if (refused) return\n const buf = typeof chunk === 'string' ? Buffer.from(chunk) : chunk\n size += buf.length\n if (size > MAX_BODY_BYTES) {\n refused = true\n chunks.length = 0\n reject(tooLarge())\n return\n }\n chunks.push(buf)\n })\n req.on('end', () => {\n if (!refused) resolve(Buffer.concat(chunks).toString('utf8'))\n })\n req.on('error', reject)\n })\n}\n\nfunction mediaTypeOf(req: IncomingMessage): string {\n return String(req.headers['content-type'] ?? '')\n .split(';')[0]!\n .trim()\n .toLowerCase()\n}\n\n/** A JSON object body. An empty body is `{}`; anything else that is not an object is refused. */\nexport async function readJsonObject(req: IncomingMessage): Promise<Record<string, unknown>> {\n const text = await readBody(req)\n if (text.trim() === '') return {}\n if (mediaTypeOf(req) !== 'application/json') {\n throw new HostError(415, 'UNSUPPORTED_MEDIA_TYPE', 'Send the body as application/json.')\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n throw new HostError(400, 'INVALID_JSON', 'The request body is not valid JSON.')\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new HostError(400, 'INVALID_BODY', 'The request body must be a JSON object.')\n }\n return parsed as Record<string, unknown>\n}\n\n/** A form-encoded body (the OAuth token endpoint's only format). */\nexport async function readForm(req: IncomingMessage): Promise<URLSearchParams> {\n const text = await readBody(req)\n if (mediaTypeOf(req) !== 'application/x-www-form-urlencoded') {\n throw new HostError(415, 'UNSUPPORTED_MEDIA_TYPE', 'Send the body as application/x-www-form-urlencoded.')\n }\n return new URLSearchParams(text)\n}\n\nexport function cookiesOf(req: IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {}\n for (const part of String(req.headers.cookie ?? '').split(';')) {\n const at = part.indexOf('=')\n if (at <= 0) continue\n const name = part.slice(0, at).trim()\n const value = part.slice(at + 1).trim()\n try {\n out[name] = decodeURIComponent(value)\n } catch {\n out[name] = value\n }\n }\n return out\n}\n\n/**\n * The visitor's credential: `x-app-session`, else `Authorization: Bearer`.\n * The session protocol accepts either on every session-server route.\n */\nexport function credentialOf(req: IncomingMessage): string | undefined {\n const session = req.headers['x-app-session']\n if (typeof session === 'string' && session.trim()) return session.trim()\n const auth = req.headers.authorization\n if (typeof auth === 'string') {\n const m = /^Bearer\\s+(\\S+)\\s*$/i.exec(auth)\n if (m) return m[1]\n }\n return undefined\n}\n\n/**\n * Only a path on the app's own origin may be the way back — the same rule the\n * runner applies before it builds a start address (`//host` and `/\\host` are\n * other sites to a browser).\n */\nexport function safeReturnTo(value: string | null): string {\n if (!value || !value.startsWith('/')) return '/'\n const second = value.charAt(1)\n return second === '/' || second === '\\\\' ? '/' : value\n}\n"]}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The local host — the session protocol and the data protocol, in process,
3
+ * for development.
4
+ *
5
+ * NOT A PRODUCTION SERVER. Records live in memory and vanish on restart; the
6
+ * one development user signs in without a password; the issuer stand-in mints
7
+ * unsigned ID tokens; there is no rate limit, no CORS, no TLS. It exists so an
8
+ * app that declares data and sign-in runs on a laptop with no hosted service,
9
+ * and so the runner's sign-in wall, account control, gate and data paths are
10
+ * exercised against the same addresses and answers a session server gives.
11
+ *
12
+ * Everything is mounted under `prefix` (e.g. `/_host`); `<base>` below is
13
+ * `<origin><prefix>`, which is also the app's own API base the runner derives
14
+ * from `loginUrl` (session-protocol §2).
15
+ *
16
+ * App door (session-protocol §4, §8)
17
+ * POST <base>/login, <base>/register any credentials → the development user
18
+ * POST <base>/logout revokes the presented token
19
+ * GET <base>/session the principal behind the token
20
+ * GET <base>/_auth/start/<method> provider sign-in, through the same redirects
21
+ * GET <base>/_auth/callback/<method> …ends here: HttpOnly cookie, back to returnTo
22
+ * GET <base>/_auth/session the cookie's session (needs x-pwapp-request: 1)
23
+ * POST <base>/_auth/logout deletes the cookie (needs x-pwapp-request: 1)
24
+ * External issuer stand-in (session-protocol §3) — see ./oidc.ts
25
+ * GET <base>/oidc/.well-known/openid-configuration, GET <base>/oidc/authorize,
26
+ * POST <base>/oidc/token
27
+ * Records (data-protocol §5) — see ./records.ts
28
+ * GET|POST <base>/records/<T>, GET <base>/records/<T>/count, GET <base>/records/<T>/aggregate,
29
+ * GET|PATCH|DELETE <base>/records/<T>/<id>
30
+ * Pages
31
+ * GET <base>/surfaces/<key>.receipt.json the receipt, entity sources rewritten to <base>,
32
+ * refused without a session when sign-in is required
33
+ *
34
+ * Every credential is accepted on `x-app-session` or `Authorization: Bearer`.
35
+ * Tokens are opaque and random, kept only as hashes, and never logged.
36
+ */
37
+ import { type AuthDeclaration } from './definitions.js';
38
+ import type { CreateLocalHost } from './types.js';
39
+ export declare const DEFAULT_DEV_USER: {
40
+ readonly id: "dev-user";
41
+ readonly name: "Developer";
42
+ readonly email: "dev@localhost";
43
+ };
44
+ /** The cookie the provider round trip leaves on the app's origin. */
45
+ export declare const SESSION_COOKIE = "pwf_dev_session";
46
+ /**
47
+ * The envelope `auth.json` for this declaration, pointed at `base` — the
48
+ * same shape a publisher writes (session-protocol §2), or `undefined` when the
49
+ * app has no way in.
50
+ *
51
+ * - `external`: `oauth2-pkce` against the local issuer stand-in, keeping the
52
+ * declared client id, scopes (plus `offline_access`, which is what makes a
53
+ * refresh token exist) and audience.
54
+ * - `app`: the password door when `password` is declared; the provider
55
+ * buttons and the start address for every declared provider method; the
56
+ * gateway's session and logout addresses for either; `methods` as declared.
57
+ */
58
+ export declare function authAddresses(auth: AuthDeclaration | undefined, base: string): Record<string, unknown> | undefined;
59
+ export declare const createLocalHost: CreateLocalHost;