@lantharos/pult 0.0.0-stage → 0.8.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.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/cli.d.mts +1 -0
- package/cli.mjs +917 -0
- package/index.d.mts +950 -0
- package/index.mjs +950 -0
- package/package.json +23 -4
- package/schema-CzG8J_9_.mjs +638 -0
- package/stub.d.mts +11 -0
- package/stub.mjs +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lantharos/pult",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "The other side of your app",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://pult.sh",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.mts",
|
|
11
|
+
"default": "./index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"pult": "./cli.mjs"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"jiti": "2.7.0",
|
|
22
|
+
"@standard-schema/spec": "1.1.0",
|
|
23
|
+
"croner": "10.0.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
//#region ../protocol/src/manifest.ts
|
|
2
|
+
const HOME = "home";
|
|
3
|
+
const FLAG_ACTION = "flag:";
|
|
4
|
+
const DEFAULT_STATES = [
|
|
5
|
+
"open",
|
|
6
|
+
"in progress",
|
|
7
|
+
"handled"
|
|
8
|
+
];
|
|
9
|
+
function splitAction(id) {
|
|
10
|
+
const dot = id.indexOf(".");
|
|
11
|
+
return dot === -1 ? {
|
|
12
|
+
resource: null,
|
|
13
|
+
name: id
|
|
14
|
+
} : {
|
|
15
|
+
resource: id.slice(0, dot),
|
|
16
|
+
name: id.slice(dot + 1)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function findActionDef(manifest, id) {
|
|
20
|
+
const { resource, name } = splitAction(id);
|
|
21
|
+
if (!manifest) return null;
|
|
22
|
+
return (resource === null ? manifest.actions[name] : manifest.resources[resource]?.actions[name]) ?? null;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region ../protocol/src/blocks.ts
|
|
26
|
+
function eachBlock(blocks, visit) {
|
|
27
|
+
for (const block of blocks) {
|
|
28
|
+
visit(block);
|
|
29
|
+
if (block.type === "columns") for (const column of block.columns) eachBlock(column, visit);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function mapBlocks(blocks, map) {
|
|
33
|
+
return blocks.flatMap((block) => {
|
|
34
|
+
const mapped = map(block);
|
|
35
|
+
if (!mapped) return [];
|
|
36
|
+
if (mapped.type !== "columns") return [mapped];
|
|
37
|
+
return [{
|
|
38
|
+
type: "columns",
|
|
39
|
+
columns: mapped.columns.map((column) => mapBlocks(column, map))
|
|
40
|
+
}];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region ../protocol/src/signing.ts
|
|
45
|
+
const SIGNATURE_TOLERANCE_MS = 3e5;
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
function parseKey(key) {
|
|
48
|
+
const match = /^pult_([a-z0-9]+)_([A-Za-z0-9_-]{32,})$/.exec(key.trim());
|
|
49
|
+
if (!match) throw new Error("Malformed Pult key");
|
|
50
|
+
return {
|
|
51
|
+
environment: match[1],
|
|
52
|
+
secret: match[2]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function randomToken(bytes = 32) {
|
|
56
|
+
return base64url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
57
|
+
}
|
|
58
|
+
async function sha256(data) {
|
|
59
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
60
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
61
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
}
|
|
63
|
+
function base64url(bytes) {
|
|
64
|
+
let binary = "";
|
|
65
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
66
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
67
|
+
}
|
|
68
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
69
|
+
function hmacKey(secret) {
|
|
70
|
+
let key = keyCache.get(secret);
|
|
71
|
+
if (!key) {
|
|
72
|
+
key = crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
73
|
+
name: "HMAC",
|
|
74
|
+
hash: "SHA-256"
|
|
75
|
+
}, false, ["sign", "verify"]);
|
|
76
|
+
keyCache.set(secret, key);
|
|
77
|
+
}
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
function payload(scope, timestamp, nonce, body) {
|
|
81
|
+
return encoder.encode(`${scope}\n${timestamp}\n${nonce}\n${body}`);
|
|
82
|
+
}
|
|
83
|
+
async function sign(secret, scope, body) {
|
|
84
|
+
const timestamp = String(Date.now());
|
|
85
|
+
const nonce = randomToken(16);
|
|
86
|
+
const mac = await crypto.subtle.sign("HMAC", await hmacKey(secret), payload(scope, timestamp, nonce, body));
|
|
87
|
+
return {
|
|
88
|
+
timestamp,
|
|
89
|
+
nonce,
|
|
90
|
+
signature: base64url(new Uint8Array(mac))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function verify(secret, scope, body, headers, now = Date.now()) {
|
|
94
|
+
const timestamp = Number(headers.timestamp);
|
|
95
|
+
if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > 3e5) return "stale";
|
|
96
|
+
const mac = fromBase64url(headers.signature);
|
|
97
|
+
if (!mac) return "invalid";
|
|
98
|
+
return await crypto.subtle.verify("HMAC", await hmacKey(secret), mac, payload(scope, headers.timestamp, headers.nonce, body)) ? null : "invalid";
|
|
99
|
+
}
|
|
100
|
+
function fromBase64url(value) {
|
|
101
|
+
try {
|
|
102
|
+
const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/"));
|
|
103
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
var NonceWindow = class {
|
|
109
|
+
ttl;
|
|
110
|
+
#seen = /* @__PURE__ */ new Map();
|
|
111
|
+
constructor(ttl = SIGNATURE_TOLERANCE_MS * 2) {
|
|
112
|
+
this.ttl = ttl;
|
|
113
|
+
}
|
|
114
|
+
claim(nonce, now = Date.now()) {
|
|
115
|
+
if (this.#seen.size > 1e4) this.#sweep(now);
|
|
116
|
+
const seenAt = this.#seen.get(nonce);
|
|
117
|
+
if (seenAt !== void 0 && now - seenAt < this.ttl) return false;
|
|
118
|
+
this.#seen.set(nonce, now);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
#sweep(now) {
|
|
122
|
+
for (const [nonce, at] of this.#seen) if (now - at >= this.ttl) this.#seen.delete(nonce);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region ../protocol/src/wire.ts
|
|
127
|
+
function refKey(ref) {
|
|
128
|
+
return `${ref.resource}:${ref.id}`;
|
|
129
|
+
}
|
|
130
|
+
const HEADER = {
|
|
131
|
+
environment: "pult-environment",
|
|
132
|
+
timestamp: "pult-timestamp",
|
|
133
|
+
nonce: "pult-nonce",
|
|
134
|
+
signature: "pult-signature"
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/define/source.ts
|
|
138
|
+
const KIND = Symbol.for("pult.kind");
|
|
139
|
+
const FRAME = /\(?((?:file:\/\/)?[^()\s]+?):(\d+):\d+\)?\s*$/;
|
|
140
|
+
const RUNTIME = /^node:|[\\/]jiti[\\/]/;
|
|
141
|
+
const UNKNOWN = {
|
|
142
|
+
file: "",
|
|
143
|
+
line: 0
|
|
144
|
+
};
|
|
145
|
+
function captureOrigin() {
|
|
146
|
+
const [own, ...callers] = ((/* @__PURE__ */ new Error()).stack?.split("\n").slice(1) ?? []).flatMap(parseFrame);
|
|
147
|
+
if (!own) return UNKNOWN;
|
|
148
|
+
const sdk = own.file.replace(/[^\\/]*$/, "");
|
|
149
|
+
return callers.find(({ file }) => !file.startsWith(sdk) && !RUNTIME.test(file)) ?? UNKNOWN;
|
|
150
|
+
}
|
|
151
|
+
function parseFrame(frame) {
|
|
152
|
+
const match = FRAME.exec(frame);
|
|
153
|
+
return match ? [{
|
|
154
|
+
file: match[1].replace(/^file:\/\//, ""),
|
|
155
|
+
line: Number(match[2])
|
|
156
|
+
}] : [];
|
|
157
|
+
}
|
|
158
|
+
function kindOf(value) {
|
|
159
|
+
return value && typeof value === "object" && KIND in value ? value[KIND] : null;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/define/page.ts
|
|
163
|
+
function view(name, options) {
|
|
164
|
+
return {
|
|
165
|
+
[KIND]: "view",
|
|
166
|
+
origin: captureOrigin(),
|
|
167
|
+
name,
|
|
168
|
+
options
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function page(name, options) {
|
|
172
|
+
return {
|
|
173
|
+
[KIND]: "page",
|
|
174
|
+
origin: captureOrigin(),
|
|
175
|
+
name,
|
|
176
|
+
options
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function home(options) {
|
|
180
|
+
const resolved = Array.isArray(options) || typeof options === "function" ? { render: options } : options;
|
|
181
|
+
return {
|
|
182
|
+
[KIND]: "page",
|
|
183
|
+
origin: captureOrigin(),
|
|
184
|
+
name: HOME,
|
|
185
|
+
options: {
|
|
186
|
+
label: "Home",
|
|
187
|
+
...resolved
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function normalizeFilter(filter = {}) {
|
|
192
|
+
const { state, ...rest } = filter;
|
|
193
|
+
return {
|
|
194
|
+
...rest,
|
|
195
|
+
...state ? { state: [state].flat() } : {}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/blocks.ts
|
|
200
|
+
function field(value) {
|
|
201
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : { date: value.toISOString() };
|
|
202
|
+
return value ?? null;
|
|
203
|
+
}
|
|
204
|
+
function fieldValues(fields) {
|
|
205
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0).map(([key, value]) => [key, field(value)]));
|
|
206
|
+
}
|
|
207
|
+
function fieldList(fields) {
|
|
208
|
+
return Object.entries(fields).filter(([, value]) => value !== void 0).map(([label, value]) => ({
|
|
209
|
+
label,
|
|
210
|
+
value: field(value)
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
function ref(resource, id, title) {
|
|
214
|
+
return {
|
|
215
|
+
resource: typeof resource === "string" ? resource : resource.name,
|
|
216
|
+
id,
|
|
217
|
+
...title ? { title } : {}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function chart(source, options = {}) {
|
|
221
|
+
const range = options.range ?? "24h";
|
|
222
|
+
if (source === "occurrences") return {
|
|
223
|
+
type: "chart",
|
|
224
|
+
metric: source,
|
|
225
|
+
range,
|
|
226
|
+
title: options.title
|
|
227
|
+
};
|
|
228
|
+
if (kindOf(source) === "inbox") {
|
|
229
|
+
const inbox = source;
|
|
230
|
+
return {
|
|
231
|
+
type: "chart",
|
|
232
|
+
inbox: inbox.name,
|
|
233
|
+
range,
|
|
234
|
+
title: options.title ?? inbox.options.label
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
type: "chart",
|
|
239
|
+
points: source.map(({ at, value }) => ({
|
|
240
|
+
at: +at,
|
|
241
|
+
value
|
|
242
|
+
})),
|
|
243
|
+
title: options.title
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function listing(target, filter) {
|
|
247
|
+
if (kindOf(target) === "view") {
|
|
248
|
+
const view = target;
|
|
249
|
+
return {
|
|
250
|
+
inbox: view.options.inbox.name,
|
|
251
|
+
filter: normalizeFilter({
|
|
252
|
+
...view.options.filter,
|
|
253
|
+
...filter
|
|
254
|
+
}),
|
|
255
|
+
sort: view.options.sort ?? "-lastSeen",
|
|
256
|
+
view: view.name,
|
|
257
|
+
label: view.options.label ?? view.name
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const inbox = target;
|
|
261
|
+
return {
|
|
262
|
+
inbox: inbox.name,
|
|
263
|
+
filter: normalizeFilter(filter),
|
|
264
|
+
sort: "-lastSeen",
|
|
265
|
+
view: null,
|
|
266
|
+
label: inbox.options.label ?? inbox.name
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const actionId = (action) => typeof action === "string" ? action : action.id;
|
|
270
|
+
function button(input) {
|
|
271
|
+
if (typeof input === "object" && "action" in input) return {
|
|
272
|
+
action: actionId(input.action),
|
|
273
|
+
record: input.record ?? null,
|
|
274
|
+
...input.input && { input: JSON.parse(JSON.stringify(input.input)) },
|
|
275
|
+
...input.label && { label: input.label }
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
action: actionId(input),
|
|
279
|
+
record: null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function bindActions(blocks, resource, record) {
|
|
283
|
+
if (!resource) return blocks;
|
|
284
|
+
const own = (action) => !action.includes(".") && kindOf(resource.actions[action]) === "action";
|
|
285
|
+
const bind = (target) => own(target.action) ? {
|
|
286
|
+
...target,
|
|
287
|
+
action: `${resource.name}.${target.action}`,
|
|
288
|
+
record: target.record ?? record
|
|
289
|
+
} : target;
|
|
290
|
+
const value = (field) => field && typeof field === "object" && "edit" in field ? { edit: bind(field.edit) } : field;
|
|
291
|
+
return mapBlocks(blocks, (block) => {
|
|
292
|
+
if (block.type === "actions") return {
|
|
293
|
+
...block,
|
|
294
|
+
actions: block.actions.map(bind)
|
|
295
|
+
};
|
|
296
|
+
if (block.type === "form") return {
|
|
297
|
+
...block,
|
|
298
|
+
form: bind(block.form)
|
|
299
|
+
};
|
|
300
|
+
if (block.type === "fields") return {
|
|
301
|
+
...block,
|
|
302
|
+
fields: block.fields.map((entry) => ({
|
|
303
|
+
...entry,
|
|
304
|
+
value: value(entry.value)
|
|
305
|
+
}))
|
|
306
|
+
};
|
|
307
|
+
if (block.type === "stats") return {
|
|
308
|
+
...block,
|
|
309
|
+
stats: block.stats.map((stat) => ({
|
|
310
|
+
...stat,
|
|
311
|
+
value: value(stat.value)
|
|
312
|
+
}))
|
|
313
|
+
};
|
|
314
|
+
if (block.type === "table") return {
|
|
315
|
+
...block,
|
|
316
|
+
rows: block.rows.map((row) => row.map(value))
|
|
317
|
+
};
|
|
318
|
+
return block;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const b = {
|
|
322
|
+
markdown: (text) => ({
|
|
323
|
+
type: "markdown",
|
|
324
|
+
text
|
|
325
|
+
}),
|
|
326
|
+
fields: (fields) => ({
|
|
327
|
+
type: "fields",
|
|
328
|
+
fields: fieldList(fields)
|
|
329
|
+
}),
|
|
330
|
+
code: (code, options = {}) => ({
|
|
331
|
+
type: "code",
|
|
332
|
+
code,
|
|
333
|
+
...options
|
|
334
|
+
}),
|
|
335
|
+
chart,
|
|
336
|
+
attachment: (attachment) => ({
|
|
337
|
+
type: "attachment",
|
|
338
|
+
...attachment
|
|
339
|
+
}),
|
|
340
|
+
record: (resource, id, title) => ({
|
|
341
|
+
type: "record",
|
|
342
|
+
ref: ref(resource, id, title)
|
|
343
|
+
}),
|
|
344
|
+
ref: (resource, id, title) => ({ record: ref(resource, id, title) }),
|
|
345
|
+
sensitive: (value, options = {}) => ({ sensitive: {
|
|
346
|
+
value: String(value),
|
|
347
|
+
...options
|
|
348
|
+
} }),
|
|
349
|
+
timeline: (entries) => ({
|
|
350
|
+
type: "timeline",
|
|
351
|
+
entries: entries.map(({ at, text }) => ({
|
|
352
|
+
at: +at,
|
|
353
|
+
text
|
|
354
|
+
}))
|
|
355
|
+
}),
|
|
356
|
+
table: (rows, columns = Object.keys(rows[0] ?? {})) => ({
|
|
357
|
+
type: "table",
|
|
358
|
+
columns,
|
|
359
|
+
rows: rows.map((row) => columns.map((column) => field(row[column])))
|
|
360
|
+
}),
|
|
361
|
+
stats: (stats) => ({
|
|
362
|
+
type: "stats",
|
|
363
|
+
stats: Object.entries(stats).filter(([, stat]) => stat !== void 0).map(([label, stat]) => stat && typeof stat === "object" && "value" in stat && !(stat instanceof Date) ? {
|
|
364
|
+
label,
|
|
365
|
+
value: field(stat.value),
|
|
366
|
+
...stat.note && { note: stat.note }
|
|
367
|
+
} : {
|
|
368
|
+
label,
|
|
369
|
+
value: field(stat)
|
|
370
|
+
})
|
|
371
|
+
}),
|
|
372
|
+
actions: (...buttons) => ({
|
|
373
|
+
type: "actions",
|
|
374
|
+
actions: buttons.map(button)
|
|
375
|
+
}),
|
|
376
|
+
form: (action) => ({
|
|
377
|
+
type: "form",
|
|
378
|
+
form: button(action)
|
|
379
|
+
}),
|
|
380
|
+
columns: (...columns) => ({
|
|
381
|
+
type: "columns",
|
|
382
|
+
columns
|
|
383
|
+
}),
|
|
384
|
+
count: (target, options = {}) => {
|
|
385
|
+
const { inbox, filter, view, label } = listing(target, options.filter);
|
|
386
|
+
return {
|
|
387
|
+
type: "count",
|
|
388
|
+
inbox,
|
|
389
|
+
filter,
|
|
390
|
+
view,
|
|
391
|
+
label: options.label ?? label
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
items: (target, options = {}) => ({
|
|
395
|
+
type: "items",
|
|
396
|
+
...listing(target, options.filter),
|
|
397
|
+
...options.label && { label: options.label },
|
|
398
|
+
limit: options.limit ?? 5
|
|
399
|
+
}),
|
|
400
|
+
flag: (flag) => ({
|
|
401
|
+
type: "flag",
|
|
402
|
+
flag: flag.name
|
|
403
|
+
}),
|
|
404
|
+
edit: (value, action, options = {}) => ({ edit: {
|
|
405
|
+
value: field(value),
|
|
406
|
+
action: actionId(action),
|
|
407
|
+
record: options.record ?? null,
|
|
408
|
+
field: options.field ?? null
|
|
409
|
+
} })
|
|
410
|
+
};
|
|
411
|
+
function isPendingAttachment(block) {
|
|
412
|
+
return block.type === "attachment" && "data" in block;
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/client/transport.ts
|
|
416
|
+
const DEFAULT_URL = "https://app.pult.sh";
|
|
417
|
+
const UPLOAD_REUSE_MS = 36e5;
|
|
418
|
+
var PultError = class extends Error {
|
|
419
|
+
status;
|
|
420
|
+
code;
|
|
421
|
+
constructor(message, status, code) {
|
|
422
|
+
super(message);
|
|
423
|
+
this.status = status;
|
|
424
|
+
this.code = code;
|
|
425
|
+
this.name = "PultError";
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
var Transport = class {
|
|
429
|
+
send;
|
|
430
|
+
key;
|
|
431
|
+
url;
|
|
432
|
+
#uploads = /* @__PURE__ */ new Map();
|
|
433
|
+
constructor(key, url, send) {
|
|
434
|
+
this.send = send;
|
|
435
|
+
this.key = parseKey(key);
|
|
436
|
+
this.url = url.replace(/\/$/, "");
|
|
437
|
+
}
|
|
438
|
+
async request(method, path, body) {
|
|
439
|
+
const payload = body === void 0 ? "" : JSON.stringify(body);
|
|
440
|
+
return this.#fetch(method, path, payload, payload, "application/json");
|
|
441
|
+
}
|
|
442
|
+
async upload(name, contentType, data) {
|
|
443
|
+
const bytes = new Uint8Array(data instanceof Blob ? await data.arrayBuffer() : data);
|
|
444
|
+
const hex = await sha256(bytes);
|
|
445
|
+
const id = `${hex}:${name}:${contentType}`;
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
const recent = this.#uploads.get(id);
|
|
448
|
+
if (recent && now - recent.at < UPLOAD_REUSE_MS) return recent.key;
|
|
449
|
+
for (const [other, { at }] of this.#uploads) {
|
|
450
|
+
if (now - at < UPLOAD_REUSE_MS) break;
|
|
451
|
+
this.#uploads.delete(other);
|
|
452
|
+
}
|
|
453
|
+
const key = this.#fetch("POST", `/v1/attachments?name=${encodeURIComponent(name)}`, bytes, hex, contentType);
|
|
454
|
+
this.#uploads.delete(id);
|
|
455
|
+
this.#uploads.set(id, {
|
|
456
|
+
key,
|
|
457
|
+
at: now
|
|
458
|
+
});
|
|
459
|
+
key.catch(() => this.#uploads.delete(id));
|
|
460
|
+
return key;
|
|
461
|
+
}
|
|
462
|
+
async #fetch(method, path, body, signed, contentType) {
|
|
463
|
+
const signature = await sign(this.key.secret, `${method} ${path}`, signed);
|
|
464
|
+
const response = await this.send(`${this.url}${path}`, {
|
|
465
|
+
method,
|
|
466
|
+
body: method === "GET" ? void 0 : body,
|
|
467
|
+
headers: {
|
|
468
|
+
"content-type": contentType,
|
|
469
|
+
[HEADER.environment]: this.key.environment,
|
|
470
|
+
[HEADER.timestamp]: signature.timestamp,
|
|
471
|
+
[HEADER.nonce]: signature.nonce,
|
|
472
|
+
[HEADER.signature]: signature.signature
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
const json = await response.json().catch(() => null);
|
|
476
|
+
if (!response.ok) throw new PultError(json?.error ?? response.statusText, response.status, json?.code);
|
|
477
|
+
return json;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/define/flag.ts
|
|
482
|
+
function flag(name, options = {}) {
|
|
483
|
+
return {
|
|
484
|
+
[KIND]: "flag",
|
|
485
|
+
origin: captureOrigin(),
|
|
486
|
+
name,
|
|
487
|
+
options: {
|
|
488
|
+
...options,
|
|
489
|
+
default: options.default ?? (options.value ? void 0 : false)
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function guards(flag) {
|
|
494
|
+
return [flag.options.guard ?? []].flat();
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/define/collect.ts
|
|
498
|
+
function collect(modules) {
|
|
499
|
+
const found = {
|
|
500
|
+
resources: /* @__PURE__ */ new Map(),
|
|
501
|
+
actions: /* @__PURE__ */ new Map(),
|
|
502
|
+
inboxes: /* @__PURE__ */ new Map(),
|
|
503
|
+
roles: /* @__PURE__ */ new Map(),
|
|
504
|
+
views: /* @__PURE__ */ new Map(),
|
|
505
|
+
pages: /* @__PURE__ */ new Map(),
|
|
506
|
+
flags: /* @__PURE__ */ new Map(),
|
|
507
|
+
notify: /* @__PURE__ */ new Map(),
|
|
508
|
+
status: null,
|
|
509
|
+
config: null
|
|
510
|
+
};
|
|
511
|
+
const claim = (map, value, kind) => {
|
|
512
|
+
const existing = map.get(value.name);
|
|
513
|
+
if (existing && existing !== value) throw new Error(`Two ${kind} are named "${value.name}"`);
|
|
514
|
+
map.set(value.name, value);
|
|
515
|
+
};
|
|
516
|
+
const add = (value) => {
|
|
517
|
+
switch (kindOf(value)) {
|
|
518
|
+
case "resource": {
|
|
519
|
+
const resource = value;
|
|
520
|
+
return claim(found.resources, resource, "resources");
|
|
521
|
+
}
|
|
522
|
+
case "action": {
|
|
523
|
+
const action = value;
|
|
524
|
+
if (action.resource === null) claim(found.actions, action, "actions");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
case "inbox": {
|
|
528
|
+
const inbox = value;
|
|
529
|
+
claim(found.inboxes, inbox, "inboxes");
|
|
530
|
+
if (inbox.options.resource) add(inbox.options.resource);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
case "role": {
|
|
534
|
+
const role = value;
|
|
535
|
+
claim(found.roles, role, "roles");
|
|
536
|
+
for (const inbox of role.options.inboxes ?? []) add(inbox);
|
|
537
|
+
for (const resource of role.options.resources ?? []) add(resource);
|
|
538
|
+
for (const action of role.options.actions ?? []) add(action);
|
|
539
|
+
for (const page of role.options.pages ?? []) add(page);
|
|
540
|
+
for (const flag of role.options.flags ?? []) add(flag);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
case "view": {
|
|
544
|
+
const view = value;
|
|
545
|
+
claim(found.views, view, "views");
|
|
546
|
+
add(view.options.inbox);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
case "page": {
|
|
550
|
+
const page = value;
|
|
551
|
+
return claim(found.pages, page, "pages");
|
|
552
|
+
}
|
|
553
|
+
case "flag": {
|
|
554
|
+
const flag = value;
|
|
555
|
+
claim(found.flags, flag, "flags");
|
|
556
|
+
if (flag.options.target) add(flag.options.target);
|
|
557
|
+
for (const guard of guards(flag)) add(guard.inbox);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
case "notify": {
|
|
561
|
+
const rule = value;
|
|
562
|
+
claim(found.notify, rule, "notifications");
|
|
563
|
+
for (const inbox of rule.options.inboxes) add(inbox);
|
|
564
|
+
for (const recipient of rule.options.to) add(recipient);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
case "status": {
|
|
568
|
+
const page = value;
|
|
569
|
+
if (found.status && found.status !== page) throw new Error("A project has one status page, and this one defines two");
|
|
570
|
+
found.status = page;
|
|
571
|
+
for (const recipient of page.options.alert ?? []) add(recipient);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
case "config": found.config = value;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
for (const exports of modules) for (const value of Object.values(exports)) add(value);
|
|
578
|
+
return found;
|
|
579
|
+
}
|
|
580
|
+
function findAction(definitions, id) {
|
|
581
|
+
const { resource, name } = splitAction(id);
|
|
582
|
+
const action = resource === null ? definitions.actions.get(name) : definitions.resources.get(resource)?.actions[name];
|
|
583
|
+
return action && kindOf(action) === "action" ? action : null;
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/schema.ts
|
|
587
|
+
const libraryOptions = { zod: {
|
|
588
|
+
unrepresentable: "any",
|
|
589
|
+
override: ({ zodSchema, jsonSchema }) => {
|
|
590
|
+
if (zodSchema._zod.def.type === "date") Object.assign(jsonSchema, {
|
|
591
|
+
type: "string",
|
|
592
|
+
format: "date-time"
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
} };
|
|
596
|
+
function toJsonSchema(schema) {
|
|
597
|
+
const standard = schema["~standard"];
|
|
598
|
+
if (!standard.jsonSchema) throw new Error(`Schemas from "${schema["~standard"].vendor}" can't describe themselves as JSON Schema. Use a library that implements Standard JSON Schema, such as zod, valibot or arktype.`);
|
|
599
|
+
const { $schema: _, ...json } = standard.jsonSchema.input({
|
|
600
|
+
target: "draft-2020-12",
|
|
601
|
+
libraryOptions: libraryOptions[standard.vendor]
|
|
602
|
+
});
|
|
603
|
+
return json;
|
|
604
|
+
}
|
|
605
|
+
var ValidationError = class extends Error {
|
|
606
|
+
issues;
|
|
607
|
+
constructor(issues) {
|
|
608
|
+
super(issues.map(describeIssue).join("; "));
|
|
609
|
+
this.issues = issues;
|
|
610
|
+
this.name = "ValidationError";
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
function describeIssue(issue) {
|
|
614
|
+
const path = issue.path?.map((segment) => typeof segment === "object" ? segment.key : segment).join(".");
|
|
615
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
616
|
+
}
|
|
617
|
+
async function validate(schema, value) {
|
|
618
|
+
const result = await schema["~standard"].validate(value);
|
|
619
|
+
if (result.issues) throw new ValidationError(result.issues);
|
|
620
|
+
return result.value;
|
|
621
|
+
}
|
|
622
|
+
function reviveDates(schema, value) {
|
|
623
|
+
if (!schema || value === null || value === void 0) return value;
|
|
624
|
+
if (schema.format === "date-time" && typeof value === "string") return new Date(value);
|
|
625
|
+
for (const branch of schema.anyOf ?? schema.oneOf ?? []) {
|
|
626
|
+
const revived = reviveDates(branch, value);
|
|
627
|
+
if (revived !== value) return revived;
|
|
628
|
+
}
|
|
629
|
+
if (Array.isArray(value) && schema.items) return value.map((entry) => reviveDates(schema.items, entry));
|
|
630
|
+
if (typeof value === "object" && schema.properties) {
|
|
631
|
+
const out = { ...value };
|
|
632
|
+
for (const [key, property] of Object.entries(schema.properties)) if (key in out) out[key] = reviveDates(property, out[key]);
|
|
633
|
+
return out;
|
|
634
|
+
}
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
//#endregion
|
|
638
|
+
export { mapBlocks as A, HEADER as C, sign as D, parseKey as E, FLAG_ACTION as M, HOME as N, verify as O, findActionDef as P, kindOf as S, NonceWindow as T, normalizeFilter as _, collect as a, KIND as b, guards as c, Transport as d, b as f, home as g, isPendingAttachment as h, validate as i, DEFAULT_STATES as j, eachBlock as k, DEFAULT_URL as l, fieldValues as m, reviveDates as n, findAction as o, bindActions as p, toJsonSchema as r, flag as s, ValidationError as t, PultError as u, page as v, refKey as w, captureOrigin as x, view as y };
|
package/stub.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/build/stub.d.ts
|
|
2
|
+
export declare const env: never;
|
|
3
|
+
export declare const exports: never;
|
|
4
|
+
export declare class WorkerEntrypoint {}
|
|
5
|
+
export declare class DurableObject {}
|
|
6
|
+
export declare class WorkflowEntrypoint {}
|
|
7
|
+
export declare class RpcTarget {}
|
|
8
|
+
export declare class EmailMessage {}
|
|
9
|
+
export declare function waitUntil(): void;
|
|
10
|
+
export declare function connect(): never;
|
|
11
|
+
//#endregion
|
package/stub.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/build/stub.ts
|
|
2
|
+
const building = (what) => /* @__PURE__ */ new Error(`${what} isn't available while pult builds your definitions. Use it inside a function instead.`);
|
|
3
|
+
function unavailable(path) {
|
|
4
|
+
return new Proxy(class {}, {
|
|
5
|
+
get: (_, name) => typeof name === "symbol" || name === "then" ? void 0 : unavailable(`${path}.${name}`),
|
|
6
|
+
apply: () => {
|
|
7
|
+
throw building(path);
|
|
8
|
+
},
|
|
9
|
+
construct: () => {
|
|
10
|
+
throw building(path);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const env = unavailable("env");
|
|
15
|
+
const exports = unavailable("exports");
|
|
16
|
+
var WorkerEntrypoint = class {};
|
|
17
|
+
var DurableObject = class {};
|
|
18
|
+
var WorkflowEntrypoint = class {};
|
|
19
|
+
var RpcTarget = class {};
|
|
20
|
+
var EmailMessage = class {};
|
|
21
|
+
function waitUntil() {}
|
|
22
|
+
function connect() {
|
|
23
|
+
throw building("A socket");
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { DurableObject, EmailMessage, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint, connect, env, exports, waitUntil };
|