@substrat-run/kernel 0.147.0 → 0.149.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/dist/attachment-sweep.d.ts +208 -0
- package/dist/attachment-sweep.d.ts.map +1 -0
- package/dist/attachment-sweep.js +318 -0
- package/dist/attachment-sweep.js.map +1 -0
- package/dist/audit-outcome.d.ts +1 -1
- package/dist/audit-outcome.d.ts.map +1 -1
- package/dist/audit-outcome.js +4 -2
- package/dist/audit-outcome.js.map +1 -1
- package/dist/export-break.d.ts +137 -0
- package/dist/export-break.d.ts.map +1 -0
- package/dist/export-break.js +219 -0
- package/dist/export-break.js.map +1 -0
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/install-receipts.d.ts +116 -0
- package/dist/install-receipts.d.ts.map +1 -0
- package/dist/install-receipts.js +162 -0
- package/dist/install-receipts.js.map +1 -0
- package/dist/invocation-line.d.ts +85 -0
- package/dist/invocation-line.d.ts.map +1 -1
- package/dist/invocation-line.js +43 -0
- package/dist/invocation-line.js.map +1 -1
- package/dist/map-bounded.d.ts +8 -0
- package/dist/map-bounded.d.ts.map +1 -0
- package/dist/map-bounded.js +16 -0
- package/dist/map-bounded.js.map +1 -0
- package/dist/migration-digest.d.ts +56 -0
- package/dist/migration-digest.d.ts.map +1 -1
- package/dist/migration-digest.js +92 -0
- package/dist/migration-digest.js.map +1 -1
- package/dist/migration-fence.d.ts +14 -0
- package/dist/migration-fence.d.ts.map +1 -0
- package/dist/migration-fence.js +515 -0
- package/dist/migration-fence.js.map +1 -0
- package/dist/migration-replay.d.ts +52 -0
- package/dist/migration-replay.d.ts.map +1 -0
- package/dist/migration-replay.js +140 -0
- package/dist/migration-replay.js.map +1 -0
- package/dist/module-migrations.d.ts +3 -3
- package/dist/module-migrations.js +3 -3
- package/dist/peer.d.ts +32 -2
- package/dist/peer.d.ts.map +1 -1
- package/dist/peer.js +100 -4
- package/dist/peer.js.map +1 -1
- package/dist/permission-eval.d.ts +8 -0
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +14 -0
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +114 -361
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +134 -561
- package/dist/platform-sweep.js.map +1 -1
- package/dist/promote-review.d.ts +125 -0
- package/dist/promote-review.d.ts.map +1 -0
- package/dist/promote-review.js +181 -0
- package/dist/promote-review.js.map +1 -0
- package/dist/schedule-state.d.ts +91 -0
- package/dist/schedule-state.d.ts.map +1 -0
- package/dist/schedule-state.js +118 -0
- package/dist/schedule-state.js.map +1 -0
- package/dist/scope-copy-ledger.d.ts +29 -0
- package/dist/scope-copy-ledger.d.ts.map +1 -1
- package/dist/scope-copy-ledger.js +23 -0
- package/dist/scope-copy-ledger.js.map +1 -1
- package/dist/scope-copy.d.ts.map +1 -1
- package/dist/scope-copy.js +3 -2
- package/dist/scope-copy.js.map +1 -1
- package/dist/scope-host.d.ts +303 -11
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-identity.d.ts +68 -0
- package/dist/scope-identity.d.ts.map +1 -0
- package/dist/scope-identity.js +75 -0
- package/dist/scope-identity.js.map +1 -0
- package/dist/scope-store.d.ts +9 -6
- package/dist/scope-store.d.ts.map +1 -1
- package/dist/scope-store.js.map +1 -1
- package/dist/spine-restore.d.ts +2 -0
- package/dist/spine-restore.d.ts.map +1 -1
- package/dist/spine-restore.js +6 -1
- package/dist/spine-restore.js.map +1 -1
- package/dist/store-driver-releases.d.ts +306 -0
- package/dist/store-driver-releases.d.ts.map +1 -0
- package/dist/store-driver-releases.js +581 -0
- package/dist/store-driver-releases.js.map +1 -0
- package/dist/table-ownership.d.ts +5 -2
- package/dist/table-ownership.d.ts.map +1 -1
- package/dist/table-ownership.js +19 -10
- package/dist/table-ownership.js.map +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/upload-link.d.ts +142 -0
- package/dist/upload-link.d.ts.map +1 -0
- package/dist/upload-link.js +441 -0
- package/dist/upload-link.js.map +1 -0
- package/dist/version-migrations.d.ts +63 -1
- package/dist/version-migrations.d.ts.map +1 -1
- package/dist/version-migrations.js +174 -8
- package/dist/version-migrations.js.map +1 -1
- package/dist/vertical-scope-stores.d.ts +43 -0
- package/dist/vertical-scope-stores.d.ts.map +1 -0
- package/dist/vertical-scope-stores.js +195 -0
- package/dist/vertical-scope-stores.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { ATTACHMENT_UPLOAD_LINK_MINTED, ATTACHMENT_UPLOAD_LINK_REDEEMED, ATTACHMENT_UPLOAD_LINK_REVOKED, UPLOAD_LINK_DEFAULT_TTL_SECONDS, UPLOAD_LINK_MAX_BYTES_CEILING, UPLOAD_LINK_REPLAY_GRACE_SECONDS, UPLOAD_LINK_SECRET_PREFIX, isUploadLinkIdempotencyKey, isUploadLinkSecretShape, principalId as principalIdSchema, substratError, uploadLinkMintInput, uploadLinkPath, } from '@substrat-run/contracts';
|
|
2
|
+
import { attachmentRecordOfRow } from './attachment-text.js';
|
|
3
|
+
import { toBase64url } from './base64url.js';
|
|
4
|
+
import { capabilityTokenHash } from './capability.js';
|
|
5
|
+
import { assertAllowed } from './permission-checker.js';
|
|
6
|
+
import { attachmentSha256 } from './scope-host.js';
|
|
7
|
+
import { ulid } from './ulid.js';
|
|
8
|
+
export const UPLOAD_LINK_DDL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS _substrat_upload_links (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
-- SHA-256 hex of the secret. The secret itself is stored NOWHERE.
|
|
12
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
13
|
+
entity_type TEXT NOT NULL,
|
|
14
|
+
entity_id TEXT NOT NULL,
|
|
15
|
+
-- NULL = the uploader names the file.
|
|
16
|
+
filename TEXT,
|
|
17
|
+
max_bytes INTEGER NOT NULL,
|
|
18
|
+
-- JSON array of media types / families, or NULL = any type.
|
|
19
|
+
content_types TEXT,
|
|
20
|
+
visibility TEXT NOT NULL,
|
|
21
|
+
-- The principal the attachment is written AS: its author, and whose \`writePermission\`
|
|
22
|
+
-- is checked again at redemption.
|
|
23
|
+
minted_by TEXT NOT NULL,
|
|
24
|
+
minted_at TEXT NOT NULL,
|
|
25
|
+
expires_at TEXT NOT NULL,
|
|
26
|
+
redeemed_at TEXT,
|
|
27
|
+
-- The attachment this link produced.
|
|
28
|
+
attachment_id TEXT,
|
|
29
|
+
-- #2315: SHA-256 hex of the \`Idempotency-Key\` the spending request carried, or NULL when it
|
|
30
|
+
-- carried none. A retry of that redemption with the same key gets the same attachment back;
|
|
31
|
+
-- the key itself is stored nowhere.
|
|
32
|
+
redeemed_key_hash TEXT,
|
|
33
|
+
-- Tombstone, never a delete: a link that was minted is evidence of why a file arrived.
|
|
34
|
+
revoked_at TEXT,
|
|
35
|
+
revoked_by TEXT
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS _substrat_upload_links_entity
|
|
38
|
+
ON _substrat_upload_links (entity_type, entity_id);
|
|
39
|
+
`;
|
|
40
|
+
/** The tables `UPLOAD_LINK_DDL` builds — what a copy of a scope leaves behind. */
|
|
41
|
+
export const UPLOAD_LINK_TABLE_NAMES = ['_substrat_upload_links'];
|
|
42
|
+
/** `reason` slugs a redemption refuses with, so the HTTP route can answer 410/413/415 and a client can act. */
|
|
43
|
+
export const UPLOAD_LINK_GONE = 'upload_link_gone';
|
|
44
|
+
export const UPLOAD_LINK_TOO_LARGE = 'upload_link_too_large';
|
|
45
|
+
export const UPLOAD_LINK_TYPE_REFUSED = 'upload_link_type_refused';
|
|
46
|
+
export const UPLOAD_LINK_WRONG_TARGET = 'upload_link_wrong_target';
|
|
47
|
+
/** A replay whose bytes are not the bytes the redemption stored (#2315). */
|
|
48
|
+
export const UPLOAD_LINK_REPLAY_MISMATCH = 'upload_link_replay_mismatch';
|
|
49
|
+
const COLUMNS = 'id, entity_type, entity_id, filename, max_bytes, content_types, visibility, minted_by, minted_at, ' +
|
|
50
|
+
'expires_at, redeemed_at, attachment_id, revoked_at, revoked_by';
|
|
51
|
+
/** A fresh link secret: 256 bits, base64url, prefixed. Only its hash is ever stored. */
|
|
52
|
+
export function mintUploadLinkSecret() {
|
|
53
|
+
return UPLOAD_LINK_SECRET_PREFIX + toBase64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A media type as the platform judges and records it: the essence only — parameters dropped,
|
|
57
|
+
* whitespace trimmed, lowercased — and `image/jpg` spelled `image/jpeg`. The route and the
|
|
58
|
+
* kernel's redemption both judge through this, so a host that calls `redeemUploadLink` with
|
|
59
|
+
* `'image/svg+xml; charset=utf-8'` meets the same rule a `curl` does.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeUploadMediaType(raw) {
|
|
62
|
+
const t = raw.split(';')[0].trim().toLowerCase();
|
|
63
|
+
return t === 'image/jpg' ? 'image/jpeg' : t;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Types a browser runs rather than shows, named one by one: HTML; `text/xsl`, which Chrome
|
|
67
|
+
* parses as HTML even under `nosniff`; `multipart/x-mixed-replace`, whose parts WebKit renders
|
|
68
|
+
* as documents even under `nosniff`; the two essences the WHATWG MIME Sniffing standard (§7.1)
|
|
69
|
+
* hands to its unknown-type sniffer, which finds HTML in an HTML body; and the JavaScript MIME
|
|
70
|
+
* type essences of the same standard. With them, by rule rather than by list, is every XML type
|
|
71
|
+
* (`isActiveContentType`): a top-level navigation renders XML as a document, where an
|
|
72
|
+
* XHTML-namespace `<script>` runs. The third essence §7.1 sniffs, the any-type wildcard, is not
|
|
73
|
+
* a type a file can be declared as at all (`isMediaTypeEssence`).
|
|
74
|
+
*/
|
|
75
|
+
export const ACTIVE_CONTENT_TYPES = new Set([
|
|
76
|
+
'text/html',
|
|
77
|
+
'application/xhtml+xml',
|
|
78
|
+
'image/svg+xml',
|
|
79
|
+
'text/xml',
|
|
80
|
+
'application/xml',
|
|
81
|
+
'text/xsl',
|
|
82
|
+
// WHATWG MIME Sniffing §7.1: sniffed as an unknown type, so an HTML body is read as HTML.
|
|
83
|
+
'application/unknown',
|
|
84
|
+
'unknown/unknown',
|
|
85
|
+
// WebKit renders each part as a document, script included, even under `nosniff`.
|
|
86
|
+
'multipart/x-mixed-replace',
|
|
87
|
+
// WHATWG MIME Sniffing §4.6, "JavaScript MIME type".
|
|
88
|
+
'application/ecmascript',
|
|
89
|
+
'application/javascript',
|
|
90
|
+
'application/x-ecmascript',
|
|
91
|
+
'application/x-javascript',
|
|
92
|
+
'text/ecmascript',
|
|
93
|
+
'text/javascript',
|
|
94
|
+
'text/javascript1.0',
|
|
95
|
+
'text/javascript1.1',
|
|
96
|
+
'text/javascript1.2',
|
|
97
|
+
'text/javascript1.3',
|
|
98
|
+
'text/javascript1.4',
|
|
99
|
+
'text/javascript1.5',
|
|
100
|
+
'text/jscript',
|
|
101
|
+
'text/livescript',
|
|
102
|
+
'text/x-ecmascript',
|
|
103
|
+
'text/x-javascript',
|
|
104
|
+
]);
|
|
105
|
+
/**
|
|
106
|
+
* Would a browser run `type` (normalized) instead of showing it, when served inline as the
|
|
107
|
+
* vertical's origin? The named set, and every XML type: `+xml` structured syntax
|
|
108
|
+
* (`image/foo+xml`, `application/rss+xml`) as well as `text/xml` and `application/xml`.
|
|
109
|
+
*/
|
|
110
|
+
export const isActiveContentType = (type) => ACTIVE_CONTENT_TYPES.has(type) || type.endsWith('+xml');
|
|
111
|
+
/**
|
|
112
|
+
* One media type essence, exactly: `type/subtype`, each an RFC 9110 token (lowercase, since it is
|
|
113
|
+
* judged normalized) less `*`. No comma, so a header listing several types is not one; no space,
|
|
114
|
+
* tab, NUL, CR or LF; one slash with something on both sides. A browser reading
|
|
115
|
+
* `image/png,text/html` keeps the LAST type it can parse, and reads `text/html,x` or
|
|
116
|
+
* `text/html x` as `text/html`, so either would let a family judge one type and a page serve
|
|
117
|
+
* another. No `*`: a wildcard is what an allowlist ENTRY may say, never what a file is, and the
|
|
118
|
+
* any-type wildcard is sniffed for HTML.
|
|
119
|
+
*/
|
|
120
|
+
const MEDIA_TYPE_ESSENCE = /^[a-z0-9!#$%&'+.^_`|~-]+\/[a-z0-9!#$%&'+.^_`|~-]+$/;
|
|
121
|
+
export const isMediaTypeEssence = (type) => MEDIA_TYPE_ESSENCE.test(type);
|
|
122
|
+
/**
|
|
123
|
+
* Is `type` admitted by `allowed`? It is normalized first (`normalizeUploadMediaType`), and a
|
|
124
|
+
* result that is not exactly one essence (`isMediaTypeEssence`) is admitted by nothing, not even
|
|
125
|
+
* an absent allowlist: it could not be judged, and it could not be served as a header. `null`
|
|
126
|
+
* admits any well-formed type. A family (`image/*`) admits its members except an active type
|
|
127
|
+
* (`isActiveContentType`): the person holding a link chooses the bytes. An exact entry admits
|
|
128
|
+
* itself, active or not, so admitting SVG or HTML is a line a reviewer reads in `PERMISSIONS.md`.
|
|
129
|
+
*/
|
|
130
|
+
export function uploadContentTypeAllowed(allowed, type) {
|
|
131
|
+
const t = normalizeUploadMediaType(type);
|
|
132
|
+
if (!isMediaTypeEssence(t))
|
|
133
|
+
return false;
|
|
134
|
+
if (allowed === null)
|
|
135
|
+
return true;
|
|
136
|
+
return allowed.some((a) => (a.endsWith('/*') ? t.startsWith(a.slice(0, -1)) && !isActiveContentType(t) : a === t));
|
|
137
|
+
}
|
|
138
|
+
const stateOf = (row, now) => row.revoked_at !== null ? 'revoked' : row.redeemed_at !== null ? 'redeemed' : row.expires_at <= now ? 'expired' : 'pending';
|
|
139
|
+
function recordOf(row, now) {
|
|
140
|
+
return {
|
|
141
|
+
id: row.id,
|
|
142
|
+
entity: { entityType: row.entity_type, entityId: row.entity_id },
|
|
143
|
+
filename: row.filename,
|
|
144
|
+
maxBytes: row.max_bytes,
|
|
145
|
+
contentTypes: row.content_types === null ? null : JSON.parse(row.content_types),
|
|
146
|
+
visibility: row.visibility,
|
|
147
|
+
mintedBy: principalIdSchema.parse(row.minted_by),
|
|
148
|
+
mintedAt: row.minted_at,
|
|
149
|
+
expiresAt: row.expires_at,
|
|
150
|
+
state: stateOf(row, now),
|
|
151
|
+
redeemedAt: row.redeemed_at,
|
|
152
|
+
attachmentId: row.attachment_id,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const MS_PER_SECOND = 1000;
|
|
156
|
+
function principalOf(subject, verb) {
|
|
157
|
+
if (subject.kind !== 'principal') {
|
|
158
|
+
throw substratError('forbidden', `${verb}: only a principal may mint or revoke an upload link — a ${subject.kind} actor has no ` +
|
|
159
|
+
'authorship to lend an attachment', { reason: 'upload_link_delegation' });
|
|
160
|
+
}
|
|
161
|
+
return subject.id;
|
|
162
|
+
}
|
|
163
|
+
export function createAttachmentVerbs(deps) {
|
|
164
|
+
const readRow = (id) => deps.sql.query(`SELECT ${COLUMNS} FROM _substrat_upload_links WHERE id = ?`, [id])[0];
|
|
165
|
+
return {
|
|
166
|
+
async mintUploadUrl(raw) {
|
|
167
|
+
deps.assertWrites('ctx.attachments.mintUploadUrl');
|
|
168
|
+
const minter = principalOf(deps.subject, 'ctx.attachments.mintUploadUrl');
|
|
169
|
+
const input = uploadLinkMintInput.parse(raw);
|
|
170
|
+
const target = deps.targetOf(input.entity.entityType);
|
|
171
|
+
if (!target) {
|
|
172
|
+
throw substratError('validation_failed', `ctx.attachments.mintUploadUrl: '${input.entity.entityType}' is not a declared attachment target`);
|
|
173
|
+
}
|
|
174
|
+
const policy = target.uploadLinks;
|
|
175
|
+
if (!policy) {
|
|
176
|
+
throw substratError('forbidden', `ctx.attachments.mintUploadUrl: '${input.entity.entityType}' does not declare uploadLinks in its attachmentTargets entry`, { reason: 'upload_links_not_declared' });
|
|
177
|
+
}
|
|
178
|
+
const maxBytes = input.maxBytes ?? policy.maxBytes;
|
|
179
|
+
if (maxBytes > policy.maxBytes || maxBytes > UPLOAD_LINK_MAX_BYTES_CEILING) {
|
|
180
|
+
throw substratError('validation_failed', `ctx.attachments.mintUploadUrl: maxBytes ${maxBytes} is above the target's ${policy.maxBytes} — a link narrows its target's policy, never widens it`);
|
|
181
|
+
}
|
|
182
|
+
// Delegation, never elevation: the key an upload checks, on the entity, as the caller.
|
|
183
|
+
assertAllowed(await deps.check(target.write, input.entity));
|
|
184
|
+
const id = ulid();
|
|
185
|
+
const secret = mintUploadLinkSecret();
|
|
186
|
+
const expiresAt = new Date(Date.parse(deps.now) + (input.ttlSeconds ?? UPLOAD_LINK_DEFAULT_TTL_SECONDS) * MS_PER_SECOND).toISOString();
|
|
187
|
+
const visibility = input.visibility ?? 'internal';
|
|
188
|
+
const contentTypes = policy.contentTypes ?? null;
|
|
189
|
+
deps.sql.exec(`INSERT INTO _substrat_upload_links
|
|
190
|
+
(id, token_hash, entity_type, entity_id, filename, max_bytes, content_types, visibility,
|
|
191
|
+
minted_by, minted_at, expires_at)
|
|
192
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
193
|
+
id,
|
|
194
|
+
await capabilityTokenHash(secret),
|
|
195
|
+
input.entity.entityType,
|
|
196
|
+
input.entity.entityId,
|
|
197
|
+
input.filename ?? null,
|
|
198
|
+
maxBytes,
|
|
199
|
+
contentTypes === null ? null : JSON.stringify(contentTypes),
|
|
200
|
+
visibility,
|
|
201
|
+
minter,
|
|
202
|
+
deps.now,
|
|
203
|
+
expiresAt,
|
|
204
|
+
]);
|
|
205
|
+
deps.minted.push(secret);
|
|
206
|
+
const payload = {
|
|
207
|
+
linkId: id,
|
|
208
|
+
entity: input.entity,
|
|
209
|
+
filename: input.filename ?? null,
|
|
210
|
+
maxBytes,
|
|
211
|
+
contentTypes,
|
|
212
|
+
visibility,
|
|
213
|
+
expiresAt,
|
|
214
|
+
mintedBy: minter,
|
|
215
|
+
};
|
|
216
|
+
deps.emit({
|
|
217
|
+
type: ATTACHMENT_UPLOAD_LINK_MINTED,
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
entity: input.entity,
|
|
220
|
+
piiClass: 'none',
|
|
221
|
+
payload,
|
|
222
|
+
});
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
secret,
|
|
226
|
+
path: uploadLinkPath(input.entity, secret),
|
|
227
|
+
expiresAt,
|
|
228
|
+
maxBytes,
|
|
229
|
+
filename: input.filename ?? null,
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
async revokeUploadLink(id) {
|
|
233
|
+
deps.assertWrites('ctx.attachments.revokeUploadLink');
|
|
234
|
+
const revoker = principalOf(deps.subject, 'ctx.attachments.revokeUploadLink');
|
|
235
|
+
const row = readRow(id);
|
|
236
|
+
if (!row)
|
|
237
|
+
throw substratError('not_found', `no upload link ${id} in this scope`);
|
|
238
|
+
const entity = { entityType: row.entity_type, entityId: row.entity_id };
|
|
239
|
+
const target = deps.targetOf(row.entity_type);
|
|
240
|
+
if (target)
|
|
241
|
+
assertAllowed(await deps.check(target.write, entity));
|
|
242
|
+
// A spent or already-revoked link has nothing left to take back.
|
|
243
|
+
if (row.revoked_at !== null || row.redeemed_at !== null)
|
|
244
|
+
return;
|
|
245
|
+
deps.sql.exec(`UPDATE _substrat_upload_links SET revoked_at = ?, revoked_by = ? WHERE id = ?`, [
|
|
246
|
+
deps.now,
|
|
247
|
+
revoker,
|
|
248
|
+
id,
|
|
249
|
+
]);
|
|
250
|
+
const payload = { linkId: id, entity, revokedBy: revoker };
|
|
251
|
+
deps.emit({
|
|
252
|
+
type: ATTACHMENT_UPLOAD_LINK_REVOKED,
|
|
253
|
+
schemaVersion: 1,
|
|
254
|
+
entity,
|
|
255
|
+
piiClass: 'none',
|
|
256
|
+
payload,
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
uploadLinks(filter) {
|
|
260
|
+
const params = [];
|
|
261
|
+
let where = '';
|
|
262
|
+
if (filter?.entity) {
|
|
263
|
+
where = 'WHERE entity_type = ? AND entity_id = ?';
|
|
264
|
+
params.push(filter.entity.entityType, filter.entity.entityId);
|
|
265
|
+
}
|
|
266
|
+
const limit = Math.min(Math.max(Math.trunc(filter?.limit ?? 50), 1), 200);
|
|
267
|
+
params.push(limit);
|
|
268
|
+
return deps.sql
|
|
269
|
+
.query(`SELECT ${COLUMNS} FROM _substrat_upload_links ${where} ORDER BY id DESC LIMIT ?`, params)
|
|
270
|
+
.map((r) => recordOf(r, deps.now));
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Redemption — the host's half. A secret in, an attachment out.
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
/** One answer for every secret that is not ours or names nothing, so a probe learns nothing it can act on. */
|
|
278
|
+
const unknownLink = () => substratError('not_found', 'This upload link is not valid.');
|
|
279
|
+
/** The 410: it was ours, and it is spent, expired or revoked. Said only to a holder of the secret. */
|
|
280
|
+
const goneLink = () => substratError('conflict', 'This upload link has been used, has expired or was revoked.', { reason: UPLOAD_LINK_GONE });
|
|
281
|
+
/** The 404 for a link that exists but was minted for another target than the one the request names. */
|
|
282
|
+
const wrongTarget = () => substratError('not_found', 'This upload link is not valid for that target.', { reason: UPLOAD_LINK_WRONG_TARGET });
|
|
283
|
+
/**
|
|
284
|
+
* The hash a redemption's idempotency key is recorded and matched as, or `undefined` for none.
|
|
285
|
+
* Judged before the link is looked up, so a malformed key is the same refusal whatever the link's
|
|
286
|
+
* state, and a well-formed one costs the same hash whether it will match or not.
|
|
287
|
+
*/
|
|
288
|
+
async function idempotencyKeyHash(key) {
|
|
289
|
+
if (key === undefined)
|
|
290
|
+
return undefined;
|
|
291
|
+
if (!isUploadLinkIdempotencyKey(key)) {
|
|
292
|
+
throw substratError('validation_failed', 'An Idempotency-Key is 16 to 255 visible ASCII characters, with no space, quote or backslash, such as a UUID.');
|
|
293
|
+
}
|
|
294
|
+
return capabilityTokenHash(key);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The link a secret names, if it is still usable for `target`. The target is judged BEFORE the
|
|
298
|
+
* state, so a request naming another entity is the same 404 whether the link is pending, used,
|
|
299
|
+
* expired or revoked: the path decides, never the link's history. Nothing here spends it.
|
|
300
|
+
*
|
|
301
|
+
* With `keyHash`, a SPENT link is a replay (#2315) when the redemption that spent it carried the
|
|
302
|
+
* same key, the link's expiry plus `UPLOAD_LINK_REPLAY_GRACE_SECONDS` has not passed, and the
|
|
303
|
+
* attachment it produced is still there and not condemned by the orphan sweep (#2314). Every
|
|
304
|
+
* other spent link, and a key that matches nothing, is the same `goneLink()` as no key at all:
|
|
305
|
+
* a holder of the secret without the key learns nothing about the redemption.
|
|
306
|
+
*/
|
|
307
|
+
async function resolveLink(sql, now, secret, target, keyHash) {
|
|
308
|
+
if (!isUploadLinkSecretShape(secret))
|
|
309
|
+
throw unknownLink();
|
|
310
|
+
const row = sql.query(`SELECT ${COLUMNS} FROM _substrat_upload_links WHERE token_hash = ?`, [await capabilityTokenHash(secret)])[0];
|
|
311
|
+
if (!row)
|
|
312
|
+
throw unknownLink();
|
|
313
|
+
if (target && (row.entity_type !== target.entityType || row.entity_id !== target.entityId))
|
|
314
|
+
throw wrongTarget();
|
|
315
|
+
if (stateOf(row, now) === 'pending')
|
|
316
|
+
return { row };
|
|
317
|
+
if (keyHash !== undefined && row.redeemed_at !== null && now < replayHorizon(row)) {
|
|
318
|
+
// The key is compared in SQL, as the secret is: a hash of it is all the row holds.
|
|
319
|
+
const attachment = sql.query(`SELECT a.id, a.entity_type, a.entity_id, a.filename, a.content_type, a.size, a.sha256,
|
|
320
|
+
a.visibility, a.created_by, a.created_at
|
|
321
|
+
FROM _substrat_upload_links l
|
|
322
|
+
JOIN _substrat_attachments a ON a.id = l.attachment_id
|
|
323
|
+
WHERE l.id = ? AND l.redeemed_key_hash = ?
|
|
324
|
+
AND NOT EXISTS (SELECT 1 FROM _substrat_attachment_condemned c WHERE c.id = a.id)`, [row.id, keyHash])[0];
|
|
325
|
+
if (attachment)
|
|
326
|
+
return { row, replay: attachmentRecordOfRow(attachment) };
|
|
327
|
+
}
|
|
328
|
+
throw goneLink();
|
|
329
|
+
}
|
|
330
|
+
/** Until when a spent link's redemption may be replayed: its expiry, plus the grace. */
|
|
331
|
+
const replayHorizon = (row) => new Date(Date.parse(row.expires_at) + UPLOAD_LINK_REPLAY_GRACE_SECONDS * MS_PER_SECOND).toISOString();
|
|
332
|
+
/**
|
|
333
|
+
* Look a secret up without spending it: `not_found` for one that names nothing, `not_found`
|
|
334
|
+
* with reason `upload_link_wrong_target` for one minted for another target than `target`
|
|
335
|
+
* (whatever its state), `conflict` with reason `upload_link_gone` for one that is used,
|
|
336
|
+
* expired or revoked — unless `idempotencyKey` is the key the spending redemption carried, and
|
|
337
|
+
* then the answer carries `replay`. A malformed key is `validation_failed`, before the lookup.
|
|
338
|
+
*/
|
|
339
|
+
export async function inspectUploadLink(deps, secret, target, idempotencyKey) {
|
|
340
|
+
const keyHash = await idempotencyKeyHash(idempotencyKey);
|
|
341
|
+
const { row, replay } = await resolveLink(deps.sql, deps.now, secret, target, keyHash);
|
|
342
|
+
const r = recordOf(row, deps.now);
|
|
343
|
+
return {
|
|
344
|
+
view: {
|
|
345
|
+
entity: r.entity,
|
|
346
|
+
filename: r.filename,
|
|
347
|
+
maxBytes: r.maxBytes,
|
|
348
|
+
contentTypes: r.contentTypes,
|
|
349
|
+
visibility: r.visibility,
|
|
350
|
+
},
|
|
351
|
+
mintedBy: r.mintedBy,
|
|
352
|
+
...(replay ? { replay } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* A replay's answer (#2315): the stored record, when `body` is the file it recorded — the same
|
|
357
|
+
* size and the same SHA-256. Anything else is refused with `conflict`, reason
|
|
358
|
+
* `upload_link_replay_mismatch`: the key named another request than this one. Writes nothing.
|
|
359
|
+
*/
|
|
360
|
+
async function judgeUploadLinkReplay(replay, body) {
|
|
361
|
+
if (body.byteLength !== replay.size || (await attachmentSha256(body)) !== replay.sha256) {
|
|
362
|
+
throw substratError('conflict', 'This Idempotency-Key was used for another file: the bytes do not match the file it stored.', { reason: UPLOAD_LINK_REPLAY_MISMATCH });
|
|
363
|
+
}
|
|
364
|
+
return replay;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The host's redemption sequence, written once for both adapters (#2315): inspect, replay or
|
|
368
|
+
* write, and — for a request carrying a key that lost the spend to the request it retries —
|
|
369
|
+
* inspect again and answer that redemption. `inspect` is the host's `inspectUploadLink` with the
|
|
370
|
+
* request's key; `write` puts the bytes under a fresh id and runs the row's transaction, removing
|
|
371
|
+
* its own bytes on a refusal. A replay never reaches `write`, so it puts nothing.
|
|
372
|
+
*/
|
|
373
|
+
export async function redeemOrReplayUploadLink(inspect, input, write) {
|
|
374
|
+
const replayed = async (replay) => ({
|
|
375
|
+
...(await judgeUploadLinkReplay(replay, input.body)),
|
|
376
|
+
replayed: true,
|
|
377
|
+
});
|
|
378
|
+
const inspection = await inspect();
|
|
379
|
+
if (inspection.replay)
|
|
380
|
+
return replayed(inspection.replay);
|
|
381
|
+
try {
|
|
382
|
+
return await write(inspection);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
const reason = err?.extensions?.reason;
|
|
386
|
+
if (input.idempotencyKey === undefined || reason !== UPLOAD_LINK_GONE)
|
|
387
|
+
throw err;
|
|
388
|
+
const again = await inspect().catch(() => undefined);
|
|
389
|
+
if (again?.replay)
|
|
390
|
+
return replayed(again.replay);
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Spend the link for `record`, INSIDE the transaction that inserts the attachment row — so a
|
|
396
|
+
* refusal anywhere rolls the spend back. Judges the record against the link, never the other
|
|
397
|
+
* way round: the target, the author, the size and the type are all the link's to refuse.
|
|
398
|
+
*/
|
|
399
|
+
export async function redeemUploadLinkInTransaction(deps, secret, record, idempotencyKey) {
|
|
400
|
+
const keyHash = await idempotencyKeyHash(idempotencyKey);
|
|
401
|
+
// The path named another entity than the link was minted for: nothing is spent. No key is
|
|
402
|
+
// passed: a spend needs a pending link, and a replay is the host's to answer before it writes.
|
|
403
|
+
const { row } = await resolveLink(deps.sql, deps.now, secret, record.entity);
|
|
404
|
+
if (record.createdBy !== row.minted_by) {
|
|
405
|
+
throw substratError('internal', 'an upload link was redeemed as someone other than its minter');
|
|
406
|
+
}
|
|
407
|
+
if (record.size > row.max_bytes) {
|
|
408
|
+
throw substratError('validation_failed', `The file is ${record.size} bytes; this link admits at most ${row.max_bytes}.`, {
|
|
409
|
+
reason: UPLOAD_LINK_TOO_LARGE,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const allowed = row.content_types === null ? null : JSON.parse(row.content_types);
|
|
413
|
+
if (!uploadContentTypeAllowed(allowed, record.contentType)) {
|
|
414
|
+
throw substratError('validation_failed', `A file of type ${record.contentType} is not admitted by this link.`, {
|
|
415
|
+
reason: UPLOAD_LINK_TYPE_REFUSED,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (row.filename !== null && record.filename !== row.filename) {
|
|
419
|
+
throw substratError('internal', 'an upload link with a fixed file name was redeemed under another');
|
|
420
|
+
}
|
|
421
|
+
const spent = deps.sql.exec(`UPDATE _substrat_upload_links SET redeemed_at = ?, attachment_id = ?, redeemed_key_hash = ?
|
|
422
|
+
WHERE id = ? AND redeemed_at IS NULL AND revoked_at IS NULL`, [deps.now, record.id, keyHash ?? null, row.id]);
|
|
423
|
+
if (spent.changes !== 1)
|
|
424
|
+
throw goneLink();
|
|
425
|
+
const payload = {
|
|
426
|
+
linkId: row.id,
|
|
427
|
+
entity: record.entity,
|
|
428
|
+
attachmentId: record.id,
|
|
429
|
+
size: record.size,
|
|
430
|
+
sha256: record.sha256,
|
|
431
|
+
mintedBy: principalIdSchema.parse(row.minted_by),
|
|
432
|
+
};
|
|
433
|
+
deps.emit({
|
|
434
|
+
type: ATTACHMENT_UPLOAD_LINK_REDEEMED,
|
|
435
|
+
schemaVersion: 1,
|
|
436
|
+
entity: record.entity,
|
|
437
|
+
piiClass: 'none',
|
|
438
|
+
payload,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=upload-link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-link.js","sourceRoot":"","sources":["../src/upload-link.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* bounded batch at a time, and until it reaches a version the read falls back to that
|
|
22
22
|
* version's manifest. The fallback is only for those rows. A version published by this code
|
|
23
23
|
* is split on the way in.
|
|
24
|
+
* - `migration_digest_derived` (#2339): 1 once `migration_digest` is the one the control plane
|
|
25
|
+
* derives from these rows and the manifest's `doClasses`, never the one the push sent. NULL is
|
|
26
|
+
* a version stored before this, whose digest is still the pushed one: a backfill re-derives
|
|
27
|
+
* those a batch at a time, and a read of one derives it first.
|
|
24
28
|
*/
|
|
25
29
|
import { type DeclaredMigration } from '@substrat-run/contracts';
|
|
26
30
|
import type { SwitchSql } from './system-switch.js';
|
|
@@ -34,7 +38,7 @@ import type { SwitchSql } from './system-switch.js';
|
|
|
34
38
|
* a directory from before #1764 lacks, so the adapters run these statements after their
|
|
35
39
|
* column additions, not in their DDL loop.
|
|
36
40
|
*/
|
|
37
|
-
export declare const VERSION_MIGRATIONS_DDL = "\n CREATE TABLE IF NOT EXISTS vertical_version_migrations (\n version_id TEXT NOT NULL,\n -- The migration's place in the order the host runs them, from 0.\n ordinal INTEGER NOT NULL,\n module_id TEXT NOT NULL,\n -- The module's SqlMigration.version, e.g. 0001-init.\n version TEXT NOT NULL,\n sql TEXT NOT NULL,\n PRIMARY KEY (version_id, ordinal)\n );\n CREATE INDEX IF NOT EXISTS vertical_versions_unsplit\n ON vertical_versions (id) WHERE migrations_split IS NULL;\n";
|
|
41
|
+
export declare const VERSION_MIGRATIONS_DDL = "\n CREATE TABLE IF NOT EXISTS vertical_version_migrations (\n version_id TEXT NOT NULL,\n -- The migration's place in the order the host runs them, from 0.\n ordinal INTEGER NOT NULL,\n module_id TEXT NOT NULL,\n -- The module's SqlMigration.version, e.g. 0001-init.\n version TEXT NOT NULL,\n sql TEXT NOT NULL,\n -- The migration's declared moves and discards (#2322), as JSON { moves?, discards? }.\n -- NULL = it declares none, or was stored before they were carried.\n declarations TEXT,\n PRIMARY KEY (version_id, ordinal)\n );\n CREATE INDEX IF NOT EXISTS vertical_versions_unsplit\n ON vertical_versions (id) WHERE migrations_split IS NULL;\n -- #2339: versions whose migration_digest is still the pushed one, not yet re-derived.\n CREATE INDEX IF NOT EXISTS vertical_versions_underived\n ON vertical_versions (id) WHERE migration_digest_derived IS NULL;\n";
|
|
38
42
|
/** A manifest with its SQL taken out, and the SQL. */
|
|
39
43
|
export interface SplitManifest {
|
|
40
44
|
/** The manifest to store: without `migrations`, or exactly as given when there was nothing to take. */
|
|
@@ -105,4 +109,62 @@ export declare function versionMigrationsOf(db: SwitchSql, versionId: string): {
|
|
|
105
109
|
verticalSlug: string;
|
|
106
110
|
migrations: DeclaredMigration[] | null;
|
|
107
111
|
} | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* The `reason` on a push refused because its `digests.migration` is not the one the control plane
|
|
114
|
+
* derives from the migrations it carries (#2339).
|
|
115
|
+
*/
|
|
116
|
+
export declare const MIGRATION_DIGEST_MISMATCH_REASON = "migration_digest_mismatch";
|
|
117
|
+
/**
|
|
118
|
+
* A version's migration digest as the control plane derives it (#2339): `migrationDigestOf`
|
|
119
|
+
* over the manifest's `doClasses` and the migrations the version stores, with what each
|
|
120
|
+
* declares. A version that stores no migrations to derive it from (`migrations` null), or whose
|
|
121
|
+
* `doClasses` do not read, gets `unheldMigrationDigest`, which equals no other version's.
|
|
122
|
+
*/
|
|
123
|
+
export declare function derivedMigrationDigest(versionId: string, doClasses: unknown, migrations: readonly DeclaredMigration[] | null): Promise<string>;
|
|
124
|
+
/**
|
|
125
|
+
* The migration digest a publish stores (#2339): the one derived from what it stores, never the
|
|
126
|
+
* one pushed. A push that carries its migrations and pushed another digest is refused
|
|
127
|
+
* (`validation_failed`, reason {@link MIGRATION_DIGEST_MISMATCH_REASON}): either its CLI hashes
|
|
128
|
+
* something other than what it sent, or it hashes less than the platform compares. One that
|
|
129
|
+
* carries none is not refused, since nothing can check it, and stores the unheld digest.
|
|
130
|
+
*/
|
|
131
|
+
export declare function publishedMigrationDigest(input: {
|
|
132
|
+
id: string;
|
|
133
|
+
migrationDigest: string;
|
|
134
|
+
manifestJson: string | null;
|
|
135
|
+
migrations: readonly DeclaredMigration[] | null;
|
|
136
|
+
}): Promise<string>;
|
|
137
|
+
/** The two reads that find versions whose digest is not derived yet, answered from `vertical_versions_underived`. */
|
|
138
|
+
export declare const UNDERIVED_IDS_SQL = "SELECT id FROM vertical_versions WHERE migration_digest_derived IS NULL ORDER BY id LIMIT ?";
|
|
139
|
+
export declare const UNDERIVED_PROBE_SQL = "SELECT 1 AS present FROM vertical_versions WHERE migration_digest_derived IS NULL LIMIT 1";
|
|
140
|
+
/**
|
|
141
|
+
* Derive one stored version's migration digest from its stored rows and record it (#2339),
|
|
142
|
+
* unless it is derived already. Returns the digest the version now holds, or undefined when
|
|
143
|
+
* there is no such version.
|
|
144
|
+
*
|
|
145
|
+
* Two statements, not a transaction: the digest is computed between them (Web Crypto is
|
|
146
|
+
* asynchronous), and the write is guarded on the row still being underived, so two derivations
|
|
147
|
+
* racing write once. A version's migrations and `doClasses` never change after it is published,
|
|
148
|
+
* so both derive the same value, and what was read is what is recorded.
|
|
149
|
+
*/
|
|
150
|
+
export declare function deriveVersionMigrationDigest(db: SwitchSql, versionId: string): Promise<string | undefined>;
|
|
151
|
+
/**
|
|
152
|
+
* Re-derive the next versions stored before #2339, whose migration digest is still the one their
|
|
153
|
+
* push sent: at most `limit` of them. Returns how many it derived, and whether any are left.
|
|
154
|
+
* Resumable and idempotent on #1764's terms: each version is marked as it is derived, and a
|
|
155
|
+
* derived one is never selected again. Until the backfill reaches a version, a read of it
|
|
156
|
+
* derives it first (`deriveVersionMigrationDigest`), so no gate compares a pushed digest.
|
|
157
|
+
*/
|
|
158
|
+
export declare function deriveMigrationDigestsBatch(db: SwitchSql, limit?: number): Promise<{
|
|
159
|
+
derived: number;
|
|
160
|
+
more: boolean;
|
|
161
|
+
}>;
|
|
162
|
+
/** Whether any version's migration digest still waits to be derived. One probe of the partial index. */
|
|
163
|
+
export declare function versionsAwaitDigest(db: SwitchSql): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* The promote gate's refusal when the migration digest moved and the change was not acknowledged.
|
|
166
|
+
* A side whose digest is unheld (#2339) is named with why it always asks, so a vertical whose
|
|
167
|
+
* pushes leave their SQL out of the manifest is not surprised that every promote does.
|
|
168
|
+
*/
|
|
169
|
+
export declare function migrationChangeRefusal(outgoing: string, incoming: string): string;
|
|
108
170
|
//# sourceMappingURL=version-migrations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-migrations.d.ts","sourceRoot":"","sources":["../src/version-migrations.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"version-migrations.d.ts","sourceRoot":"","sources":["../src/version-migrations.ts"],"names":[],"mappings":"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"}
|