@nacre.work/core 0.1.0 → 0.3.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/audit.d.ts +41 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +18 -0
- package/dist/audit.js.map +1 -0
- package/dist/authz/cache.d.ts +20 -0
- package/dist/authz/cache.d.ts.map +1 -1
- package/dist/authz/cache.js +29 -0
- package/dist/authz/cache.js.map +1 -1
- package/dist/authz/index.d.ts +1 -2
- package/dist/authz/index.d.ts.map +1 -1
- package/dist/authz/index.js +1 -2
- package/dist/authz/index.js.map +1 -1
- package/dist/config.d.ts +132 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +439 -22
- package/dist/config.js.map +1 -1
- package/dist/db/migrate.d.ts +45 -0
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +61 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/extensions.d.ts +298 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +246 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +13 -26
- package/dist/lifecycle.js.map +1 -1
- package/dist/logging.d.ts +46 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +110 -0
- package/dist/logging.js.map +1 -0
- package/dist/metrics.d.ts +11 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +32 -7
- package/dist/metrics.js.map +1 -1
- package/dist/migrations/0014_retired_collections.sql +68 -0
- package/dist/migrations/0015_reference_queries.sql +97 -0
- package/dist/migrations/0016_drop_acl_tag_cache.sql +62 -0
- package/dist/migrations/0017_drop_workspace_admin_role.sql +30 -0
- package/dist/migrations/0018_group_members_nulls_not_distinct.sql +46 -0
- package/dist/migrations/0019_embedding_provider_write_guard.sql +58 -0
- package/dist/multipart.d.ts +55 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +158 -0
- package/dist/multipart.js.map +1 -0
- package/dist/oauth.d.ts +10 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +10 -0
- package/dist/oauth.js.map +1 -1
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +43 -46
- package/dist/observability.js.map +1 -1
- package/dist/reindex.d.ts +44 -0
- package/dist/reindex.d.ts.map +1 -1
- package/dist/reindex.js +49 -0
- package/dist/reindex.js.map +1 -1
- package/dist/s3.d.ts +111 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +242 -0
- package/dist/s3.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vector/query.d.ts +7 -9
- package/dist/vector/query.d.ts.map +1 -1
- package/dist/vector/query.js +7 -6
- package/dist/vector/query.js.map +1 -1
- package/dist/vector/search.d.ts +86 -1
- package/dist/vector/search.d.ts.map +1 -1
- package/dist/vector/search.js +185 -1
- package/dist/vector/search.js.map +1 -1
- package/package.json +1 -1
- package/dist/authz/tags.d.ts +0 -40
- package/dist/authz/tags.d.ts.map +0 -1
- package/dist/authz/tags.js +0 -48
- package/dist/authz/tags.js.map +0 -1
package/dist/observability.js
CHANGED
|
@@ -15,11 +15,12 @@ export function collectDatabaseGauges(pool, metrics, role) {
|
|
|
15
15
|
// Across organizations, so it runs outside withOrg under the role that
|
|
16
16
|
// may see them all. Every query below names org_id in its output rather
|
|
17
17
|
// than filtering to one.
|
|
18
|
-
const { rows: orgs } = await client.query('SELECT id, slug
|
|
18
|
+
const { rows: orgs } = await client.query('SELECT id, slug FROM organizations WHERE deleted_at IS NULL');
|
|
19
19
|
metrics.documents.reset();
|
|
20
20
|
metrics.tombstonesPending.reset();
|
|
21
|
-
metrics.
|
|
21
|
+
metrics.processingAge.reset();
|
|
22
22
|
metrics.reindexProgress.reset();
|
|
23
|
+
metrics.collectionsRetired.reset();
|
|
23
24
|
// One series per layer that has ever been reindexed, labelled by slug
|
|
24
25
|
// like every other per-tenant gauge here. Layers with no reindex produce
|
|
25
26
|
// no row and therefore no series: inventing a zero for them would mean
|
|
@@ -41,6 +42,24 @@ export function collectDatabaseGauges(pool, metrics, role) {
|
|
|
41
42
|
continue;
|
|
42
43
|
metrics.reindexProgress.set(reindexProgress(state), { org: row.slug, layer: row.layer });
|
|
43
44
|
}
|
|
45
|
+
// Collections a finished migration is still holding, for the same reason
|
|
46
|
+
// and in the same shape as the reindex gauge above: one query across
|
|
47
|
+
// organizations, because `retired_collections` holds a handful of rows in
|
|
48
|
+
// total rather than a handful per tenant, and a scrape must not cost a
|
|
49
|
+
// query per organization.
|
|
50
|
+
//
|
|
51
|
+
// Only organizations that have one produce a series. A zero for everybody
|
|
52
|
+
// else would be true and useless — the number that matters is how much
|
|
53
|
+
// disk a completed migration has not given back, and the alertable shape
|
|
54
|
+
// is one that stops falling, which needs a series to exist at all rather
|
|
55
|
+
// than a floor under every tenant.
|
|
56
|
+
const { rows: retained } = await client.query(`SELECT o.slug, count(*) AS n
|
|
57
|
+
FROM retired_collections r
|
|
58
|
+
JOIN organizations o ON o.id = r.org_id
|
|
59
|
+
GROUP BY o.slug`);
|
|
60
|
+
for (const row of retained) {
|
|
61
|
+
metrics.collectionsRetired.set(Number(row.n), { org: row.slug });
|
|
62
|
+
}
|
|
44
63
|
for (const org of orgs) {
|
|
45
64
|
// One query per organization, not three.
|
|
46
65
|
//
|
|
@@ -57,62 +76,40 @@ export function collectDatabaseGauges(pool, metrics, role) {
|
|
|
57
76
|
// still inside `withOrg`: collapsing across tenants would mean reading
|
|
58
77
|
// every tenant's documents from the API process, and the row-level
|
|
59
78
|
// policies are not something to route around for a gauge.
|
|
60
|
-
const rows = await withOrg(pool, org.id, async (c) => (await c.query(
|
|
79
|
+
const rows = await withOrg(pool, org.id, async (c) => (await c.query(
|
|
80
|
+
// claim_age rides this query rather than adding one: claimed_at
|
|
81
|
+
// is on the same documents rows, non-null only while a document
|
|
82
|
+
// is being indexed, and cleared on success or failure. The max
|
|
83
|
+
// per status is the oldest in-flight document; the max across
|
|
84
|
+
// statuses is taken below, so a worker wedged in either
|
|
85
|
+
// `parsing` or `indexing` shows up.
|
|
86
|
+
`SELECT status,
|
|
61
87
|
count(*) FILTER (WHERE deleted_at IS NULL)::text AS live,
|
|
62
88
|
count(*) FILTER (
|
|
63
89
|
WHERE deleted_at IS NOT NULL AND vectors_purged_at IS NULL
|
|
64
90
|
)::text AS tombstoned,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-- number is the age of the oldest document still
|
|
69
|
-
-- carrying tags built from a superseded grant set, which
|
|
70
|
-
-- only a document with points in the index can do.
|
|
71
|
-
-- Without the clause a single failed or pending
|
|
72
|
-
-- document pinned the gauge at its own age forever,
|
|
73
|
-
-- because the retag loop claims neither and nothing else
|
|
74
|
-
-- can clear it — the one metric with an alert on it
|
|
75
|
-
-- became a stuck alert, which is how it gets muted.
|
|
76
|
-
--
|
|
77
|
-
-- Deliberately not status = 'indexed'. A document that
|
|
78
|
-
-- indexed once and failed on a later pass still has the
|
|
79
|
-
-- earlier pass's points and still carries their tags,
|
|
80
|
-
-- and is exactly the case this is meant to catch.
|
|
81
|
-
-- claimStale uses the same predicate; the two must
|
|
82
|
-
-- agree, or the gauge counts what the loop cannot claim.
|
|
83
|
-
min(COALESCE(acl_tagged_at, created_at)) FILTER (
|
|
84
|
-
WHERE deleted_at IS NULL AND chunk_count > 0
|
|
85
|
-
AND acl_version < $2
|
|
86
|
-
)::text AS oldest_stale
|
|
91
|
+
extract(
|
|
92
|
+
epoch FROM max(now() - claimed_at) FILTER (WHERE claimed_at IS NOT NULL)
|
|
93
|
+
) AS claim_age
|
|
87
94
|
FROM documents
|
|
88
95
|
WHERE org_id = $1
|
|
89
|
-
GROUP BY status`, [org.id
|
|
96
|
+
GROUP BY status`, [org.id])).rows, scope);
|
|
90
97
|
let tombstoned = 0;
|
|
91
|
-
let
|
|
98
|
+
let claimAge = 0;
|
|
92
99
|
for (const row of rows) {
|
|
93
100
|
metrics.documents.set(Number(row.live), { org: org.slug, status: row.status });
|
|
94
101
|
tombstoned += Number(row.tombstoned);
|
|
95
|
-
if (row.
|
|
96
|
-
|
|
97
|
-
if (!Number.isNaN(at) && (oldest === undefined || at < oldest))
|
|
98
|
-
oldest = at;
|
|
99
|
-
}
|
|
102
|
+
if (row.claim_age !== null)
|
|
103
|
+
claimAge = Math.max(claimAge, Number(row.claim_age));
|
|
100
104
|
}
|
|
101
105
|
metrics.tombstonesPending.set(tombstoned, { org: org.slug });
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// aggregate cost nothing and bought nothing.
|
|
110
|
-
//
|
|
111
|
-
// Zero is written rather than omitted: an absent series and a zero one
|
|
112
|
-
// mean "caught up" and "not being measured", and those must not look
|
|
113
|
-
// alike on the one metric that evidences I4.
|
|
114
|
-
const lag = oldest === undefined ? 0 : Math.max(0, (Date.now() - oldest) / 1000);
|
|
115
|
-
metrics.aclPropagationLag.set(lag, { org: org.slug });
|
|
106
|
+
// Only when something is in flight. A zero for every idle tenant would
|
|
107
|
+
// be true and useless; the alertable shape is a series that exists and
|
|
108
|
+
// climbs, so no claimed document means no series, the same way the
|
|
109
|
+
// reindex and retired-collection gauges stay silent about tenants with
|
|
110
|
+
// nothing to report.
|
|
111
|
+
if (claimAge > 0)
|
|
112
|
+
metrics.processingAge.set(claimAge, { org: org.slug });
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAG7D;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAU,EAAE,OAAgB,EAAE,IAAa;IAC/E,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;IAEhD,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACnC,IAAI,CAAC;YACH,uEAAuE;YACvE,wEAAwE;YACxE,yBAAyB;YACzB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACvC,
|
|
1
|
+
{"version":3,"file":"observability.js","sourceRoot":"","sources":["../observability.ts"],"names":[],"mappings":"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"}
|
package/dist/reindex.d.ts
CHANGED
|
@@ -59,6 +59,40 @@ export interface ReindexState {
|
|
|
59
59
|
readonly done: number;
|
|
60
60
|
readonly failed: number;
|
|
61
61
|
readonly error?: string;
|
|
62
|
+
/**
|
|
63
|
+
* What the recall check scored, once it has run.
|
|
64
|
+
*
|
|
65
|
+
* Absent until then, and absent forever for a layer with no reference query
|
|
66
|
+
* set — that layer has no gate, and `finishReindexIfDone` says so by asking
|
|
67
|
+
* whether the set exists rather than by looking for this key.
|
|
68
|
+
*/
|
|
69
|
+
readonly check?: ReindexCheck;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The verdict of the gate between a finished reindex and its switch.
|
|
73
|
+
*
|
|
74
|
+
* `scores` is keyed by reference query id and not by text. The text is the
|
|
75
|
+
* operator's own, so echoing it would break no rule — but a status endpoint and
|
|
76
|
+
* a jsonb column are both read by more people than wrote the set, and the
|
|
77
|
+
* endpoint that lists it is one join away.
|
|
78
|
+
*/
|
|
79
|
+
export interface ReindexCheck {
|
|
80
|
+
/** The mean of `scores`, in [0, 1]. */
|
|
81
|
+
readonly recall: number;
|
|
82
|
+
readonly floor: number;
|
|
83
|
+
readonly passed: boolean;
|
|
84
|
+
readonly queries: number;
|
|
85
|
+
readonly scores: readonly {
|
|
86
|
+
readonly queryId: string;
|
|
87
|
+
readonly recall: number;
|
|
88
|
+
}[];
|
|
89
|
+
/**
|
|
90
|
+
* External ids in the set naming no live document. Any means `passed` is
|
|
91
|
+
* false whatever `recall` says: a stale reference set and a bad model are
|
|
92
|
+
* different problems, and scoring a missing document as a miss reports the
|
|
93
|
+
* first as the second.
|
|
94
|
+
*/
|
|
95
|
+
readonly unresolved?: readonly string[];
|
|
62
96
|
}
|
|
63
97
|
/**
|
|
64
98
|
* How far along, as a ratio, for `nacre_reindex_progress_ratio`.
|
|
@@ -75,6 +109,16 @@ export interface ReindexState {
|
|
|
75
109
|
export declare function reindexProgress(state: ReindexState): number;
|
|
76
110
|
/** The JSON shape stored in the column. Snake case, like every other jsonb here. */
|
|
77
111
|
export declare function toStateJson(state: ReindexState): Record<string, unknown>;
|
|
112
|
+
/**
|
|
113
|
+
* The verdict as it is stored, snake case like the rest of the column.
|
|
114
|
+
*
|
|
115
|
+
* Its own function because the worker writes this key on its own — one
|
|
116
|
+
* `jsonb_set` rather than a rewrite of the whole state — and a second
|
|
117
|
+
* hand-written shape there is how the two would drift. `passed` is read back
|
|
118
|
+
* out of this object by the SQL predicate that performs the switch, so its name
|
|
119
|
+
* and its type are load-bearing rather than cosmetic.
|
|
120
|
+
*/
|
|
121
|
+
export declare function toCheckJson(check: ReindexCheck): Record<string, unknown>;
|
|
78
122
|
/**
|
|
79
123
|
* Read it back, or `undefined` for a column that holds nothing usable.
|
|
80
124
|
*
|
package/dist/reindex.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../reindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAA;IAClD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAA;IACvC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"reindex.d.ts","sourceRoot":"","sources":["../reindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAA;IAClD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,WAAW,CAAA;IACvC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAA;CAC9B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACjF;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACxC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAQ3D;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASxE;AAyBD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CA0BpE"}
|
package/dist/reindex.js
CHANGED
|
@@ -60,6 +60,51 @@ export function toStateJson(state) {
|
|
|
60
60
|
done: state.done,
|
|
61
61
|
failed: state.failed,
|
|
62
62
|
...(state.error === undefined ? {} : { error: state.error }),
|
|
63
|
+
...(state.check === undefined ? {} : { check: toCheckJson(state.check) }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The verdict as it is stored, snake case like the rest of the column.
|
|
68
|
+
*
|
|
69
|
+
* Its own function because the worker writes this key on its own — one
|
|
70
|
+
* `jsonb_set` rather than a rewrite of the whole state — and a second
|
|
71
|
+
* hand-written shape there is how the two would drift. `passed` is read back
|
|
72
|
+
* out of this object by the SQL predicate that performs the switch, so its name
|
|
73
|
+
* and its type are load-bearing rather than cosmetic.
|
|
74
|
+
*/
|
|
75
|
+
export function toCheckJson(check) {
|
|
76
|
+
return {
|
|
77
|
+
recall: check.recall,
|
|
78
|
+
floor: check.floor,
|
|
79
|
+
passed: check.passed,
|
|
80
|
+
queries: check.queries,
|
|
81
|
+
scores: check.scores.map((s) => ({ query_id: s.queryId, recall: s.recall })),
|
|
82
|
+
...(check.unresolved === undefined ? {} : { unresolved: [...check.unresolved] }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function fromCheckJson(raw) {
|
|
86
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
87
|
+
return undefined;
|
|
88
|
+
const o = raw;
|
|
89
|
+
if (typeof o.recall !== 'number' || typeof o.passed !== 'boolean')
|
|
90
|
+
return undefined;
|
|
91
|
+
const scores = Array.isArray(o.scores) ? o.scores : [];
|
|
92
|
+
return {
|
|
93
|
+
recall: o.recall,
|
|
94
|
+
floor: typeof o.floor === 'number' ? o.floor : 0,
|
|
95
|
+
passed: o.passed,
|
|
96
|
+
queries: typeof o.queries === 'number' ? o.queries : scores.length,
|
|
97
|
+
scores: scores.flatMap((s) => {
|
|
98
|
+
if (s === null || typeof s !== 'object')
|
|
99
|
+
return [];
|
|
100
|
+
const e = s;
|
|
101
|
+
if (typeof e.query_id !== 'string' || typeof e.recall !== 'number')
|
|
102
|
+
return [];
|
|
103
|
+
return [{ queryId: e.query_id, recall: e.recall }];
|
|
104
|
+
}),
|
|
105
|
+
...(Array.isArray(o.unresolved) && o.unresolved.length > 0
|
|
106
|
+
? { unresolved: o.unresolved.filter((u) => typeof u === 'string') }
|
|
107
|
+
: {}),
|
|
63
108
|
};
|
|
64
109
|
}
|
|
65
110
|
/**
|
|
@@ -92,6 +137,10 @@ export function fromStateJson(raw) {
|
|
|
92
137
|
done: typeof o.done === 'number' ? o.done : 0,
|
|
93
138
|
failed: typeof o.failed === 'number' ? o.failed : 0,
|
|
94
139
|
...(typeof o.error === 'string' ? { error: o.error } : {}),
|
|
140
|
+
...(() => {
|
|
141
|
+
const check = fromCheckJson(o.check);
|
|
142
|
+
return check === undefined ? {} : { check };
|
|
143
|
+
})(),
|
|
95
144
|
};
|
|
96
145
|
}
|
|
97
146
|
//# sourceMappingURL=reindex.js.map
|
package/dist/reindex.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reindex.js","sourceRoot":"","sources":["../reindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"reindex.js","sourceRoot":"","sources":["../reindex.ts"],"names":[],"mappings":"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"}
|
package/dist/s3.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object storage, signed by hand.
|
|
3
|
+
*
|
|
4
|
+
* `NACRE_S3_*` has been in `docs/config.md` since before there was a server and
|
|
5
|
+
* read by nothing — not used, not validated, so a wrong endpoint or a missing
|
|
6
|
+
* credential was silent while the `full` profile started a MinIO nobody talked
|
|
7
|
+
* to. This is the client that ends that.
|
|
8
|
+
*
|
|
9
|
+
* ─── why not the AWS SDK ───
|
|
10
|
+
*
|
|
11
|
+
* Four operations are needed: put, get, delete, head. `@aws-sdk/client-s3` is
|
|
12
|
+
* tens of megabytes and hundreds of transitive packages, in a container whose
|
|
13
|
+
* job is to read documents other people send it — which is the argument
|
|
14
|
+
* `metrics.ts` already makes for not taking prom-client. SigV4 is a documented
|
|
15
|
+
* algorithm with a published set of test vectors, it fits in this file, and it
|
|
16
|
+
* has no release cadence to track.
|
|
17
|
+
*
|
|
18
|
+
* The rule that comes with that choice: **anything here is verified against a
|
|
19
|
+
* real MinIO before it is believed**, because a signing bug produces a 403 that
|
|
20
|
+
* says nothing about which of the six inputs was wrong.
|
|
21
|
+
*
|
|
22
|
+
* ─── what is deliberately not here ───
|
|
23
|
+
*
|
|
24
|
+
* No multipart upload, so an object is one PUT and `NACRE_MAX_DOCUMENT_BYTES`
|
|
25
|
+
* bounds it. No listing, because nothing needs to enumerate a bucket and a
|
|
26
|
+
* paginated list is where this file would stop being small. No retries: the
|
|
27
|
+
* callers are the ingest queue and the collector, both of which already retry
|
|
28
|
+
* whole units of work and would otherwise retry twice.
|
|
29
|
+
*/
|
|
30
|
+
export interface S3Options {
|
|
31
|
+
/** Including the scheme and port, e.g. `http://minio:9000`. No trailing slash. */
|
|
32
|
+
readonly endpoint: string;
|
|
33
|
+
readonly bucket: string;
|
|
34
|
+
readonly region: string;
|
|
35
|
+
readonly accessKey: string;
|
|
36
|
+
readonly secretKey: string;
|
|
37
|
+
/**
|
|
38
|
+
* `{endpoint}/{bucket}/{key}` rather than `{bucket}.{endpoint}/{key}`.
|
|
39
|
+
*
|
|
40
|
+
* True for MinIO and for anything reached by IP or through a name that is not
|
|
41
|
+
* a wildcard DNS entry, which is every self-hosted deployment. AWS itself
|
|
42
|
+
* wants it false.
|
|
43
|
+
*/
|
|
44
|
+
readonly forcePathStyle: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare class S3Error extends Error {
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly method: string;
|
|
49
|
+
readonly key: string;
|
|
50
|
+
constructor(status: number, method: string, key: string, body: string);
|
|
51
|
+
}
|
|
52
|
+
export declare class S3 {
|
|
53
|
+
#private;
|
|
54
|
+
constructor(options: S3Options);
|
|
55
|
+
get bucket(): string;
|
|
56
|
+
/** Store an object, overwriting whatever was there. */
|
|
57
|
+
put(key: string, body: Uint8Array, contentType?: string): Promise<void>;
|
|
58
|
+
/** Fetch an object. `undefined` when it is not there, which is not an error. */
|
|
59
|
+
get(key: string): Promise<Uint8Array | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* Remove an object. Absent is success.
|
|
62
|
+
*
|
|
63
|
+
* S3 answers `204` whether or not the key existed, and the caller — the
|
|
64
|
+
* collector, purging a deleted document — must be able to run twice.
|
|
65
|
+
*/
|
|
66
|
+
remove(key: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* A URL that fetches one object, for a while, without a credential.
|
|
69
|
+
*
|
|
70
|
+
* Query-string SigV4 rather than the header form: the point is a link a
|
|
71
|
+
* client can follow, so everything that authenticates it has to be in the
|
|
72
|
+
* URL. `host` is the only signed header, and the payload is unsigned because
|
|
73
|
+
* a GET has none.
|
|
74
|
+
*
|
|
75
|
+
* **This is a bearer capability and it leaves the permission model behind.**
|
|
76
|
+
* Nacre checks `read` once, when the link is minted, and the object store
|
|
77
|
+
* checks nothing afterwards — a revocation during the window does not reach a
|
|
78
|
+
* URL already handed out, and neither does a delete. That is what presigning
|
|
79
|
+
* is, in every implementation of it, and it is why the lifetime is a
|
|
80
|
+
* configured number rather than a constant: `NACRE_PRESIGN_TTL` is how long a
|
|
81
|
+
* deployment is willing for that to be true.
|
|
82
|
+
*
|
|
83
|
+
* It is also why the caller of this decides, not this: minting one per search
|
|
84
|
+
* hit would issue ten capabilities to answer a question about relevance, most
|
|
85
|
+
* never followed. See the note on the search response.
|
|
86
|
+
*/
|
|
87
|
+
presign(key: string, ttlSeconds: number): string;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the bucket answers, for readiness.
|
|
90
|
+
*
|
|
91
|
+
* A HEAD on the bucket rather than on an object: it needs no key to exist and
|
|
92
|
+
* it fails for the three reasons that matter — unreachable, wrong bucket,
|
|
93
|
+
* wrong credential. A TCP connect would pass all three.
|
|
94
|
+
*/
|
|
95
|
+
ready(): Promise<boolean>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Where a document's bytes live.
|
|
99
|
+
*
|
|
100
|
+
* Derived from identity — organization, layer, external id — and never from the
|
|
101
|
+
* content. Content addressing would let two documents share an object, and then
|
|
102
|
+
* deleting one would either strand the other or need a reference count that
|
|
103
|
+
* nothing else in this system has. One document, one object, and a re-ingest
|
|
104
|
+
* overwrites in place.
|
|
105
|
+
*
|
|
106
|
+
* The external id is hashed rather than escaped: it is caller-chosen text that
|
|
107
|
+
* may contain anything, and a key derived by escaping is a key whose length and
|
|
108
|
+
* shape a caller controls.
|
|
109
|
+
*/
|
|
110
|
+
export declare function documentKey(orgId: string, layerId: string, externalId: string): string;
|
|
111
|
+
//# sourceMappingURL=s3.d.ts.map
|
package/dist/s3.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../s3.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,SAAS;IACxB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAA;CACjC;AA8BD,qBAAa,OAAQ,SAAQ,KAAK;IAE9B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAFX,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM;CAQf;AAED,qBAAa,EAAE;;gBAGD,OAAO,EAAE,SAAS;IAI9B,IAAI,MAAM,IAAI,MAAM,CAEnB;IAuGD,uDAAuD;IACjD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,SAA6B,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjG,gFAAgF;IAC1E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAOvD;;;;;OAKG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IA2ChD;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;CAiBhC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtF"}
|
package/dist/s3.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
2
|
+
const UNSIGNED = 'UNSIGNED-PAYLOAD';
|
|
3
|
+
const ALGORITHM = 'AWS4-HMAC-SHA256';
|
|
4
|
+
const SERVICE = 's3';
|
|
5
|
+
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
6
|
+
const hmac = (key, data) => createHmac('sha256', key).update(data, 'utf8').digest();
|
|
7
|
+
/**
|
|
8
|
+
* Percent-encode one path segment the way SigV4 wants it.
|
|
9
|
+
*
|
|
10
|
+
* `encodeURIComponent` leaves `!'()*` alone and AWS does not, and it encodes
|
|
11
|
+
* nothing else differently — so this is that function plus five characters. A
|
|
12
|
+
* key containing any of them signs one way and is requested another, which is a
|
|
13
|
+
* 403 with no clue in it.
|
|
14
|
+
*/
|
|
15
|
+
function encodeSegment(segment) {
|
|
16
|
+
return encodeURIComponent(segment).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
17
|
+
}
|
|
18
|
+
/** Keys are `a/b/c`; each segment is encoded and the separators are not. */
|
|
19
|
+
const encodeKey = (key) => key.split('/').map(encodeSegment).join('/');
|
|
20
|
+
export class S3Error extends Error {
|
|
21
|
+
status;
|
|
22
|
+
method;
|
|
23
|
+
key;
|
|
24
|
+
constructor(status, method, key, body) {
|
|
25
|
+
// The body, because S3 and MinIO both answer with an XML document naming
|
|
26
|
+
// the exact code — SignatureDoesNotMatch, NoSuchBucket, AccessDenied — and
|
|
27
|
+
// a status alone sends the reader to the wrong one of six inputs.
|
|
28
|
+
super(`s3 ${method} ${key} failed: ${status} ${body.slice(0, 512)}`);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.method = method;
|
|
31
|
+
this.key = key;
|
|
32
|
+
this.name = 'S3Error';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class S3 {
|
|
36
|
+
#options;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.#options = { ...options, endpoint: options.endpoint.replace(/\/+$/, '') };
|
|
39
|
+
}
|
|
40
|
+
get bucket() {
|
|
41
|
+
return this.#options.bucket;
|
|
42
|
+
}
|
|
43
|
+
#url(key) {
|
|
44
|
+
const { endpoint, bucket, forcePathStyle } = this.#options;
|
|
45
|
+
if (forcePathStyle)
|
|
46
|
+
return new URL(`${endpoint}/${bucket}/${encodeKey(key)}`);
|
|
47
|
+
const url = new URL(endpoint);
|
|
48
|
+
url.host = `${bucket}.${url.host}`;
|
|
49
|
+
url.pathname = `/${encodeKey(key)}`;
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sign a request, returning the headers to send with it.
|
|
54
|
+
*
|
|
55
|
+
* The payload hash goes in `x-amz-content-sha256` and into the canonical
|
|
56
|
+
* request, so a body that changes in flight fails the signature rather than
|
|
57
|
+
* being stored. `UNSIGNED-PAYLOAD` is used for GET and DELETE, which have
|
|
58
|
+
* none — hashing the empty string would also work and this is what every
|
|
59
|
+
* other implementation sends.
|
|
60
|
+
*/
|
|
61
|
+
#sign(input) {
|
|
62
|
+
const { region, accessKey, secretKey } = this.#options;
|
|
63
|
+
const amzDate = input.now.toISOString().replace(/[-:]|\.\d{3}/g, '');
|
|
64
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
65
|
+
const headers = {
|
|
66
|
+
...input.headers,
|
|
67
|
+
host: input.url.host,
|
|
68
|
+
'x-amz-content-sha256': input.payloadHash,
|
|
69
|
+
'x-amz-date': amzDate,
|
|
70
|
+
};
|
|
71
|
+
// Lowercased, sorted, values trimmed — the canonical form. Getting the
|
|
72
|
+
// order wrong is a 403 that reads exactly like a wrong secret.
|
|
73
|
+
const canonicalHeaderNames = Object.keys(headers)
|
|
74
|
+
.map((h) => h.toLowerCase())
|
|
75
|
+
.sort();
|
|
76
|
+
const canonicalHeaders = canonicalHeaderNames
|
|
77
|
+
.map((name) => {
|
|
78
|
+
const key = Object.keys(headers).find((h) => h.toLowerCase() === name);
|
|
79
|
+
return `${name}:${String(headers[key]).trim()}\n`;
|
|
80
|
+
})
|
|
81
|
+
.join('');
|
|
82
|
+
const signedHeaders = canonicalHeaderNames.join(';');
|
|
83
|
+
const canonicalRequest = [
|
|
84
|
+
input.method,
|
|
85
|
+
input.url.pathname,
|
|
86
|
+
// No request here takes a query string. An empty one is still a line.
|
|
87
|
+
'',
|
|
88
|
+
canonicalHeaders,
|
|
89
|
+
signedHeaders,
|
|
90
|
+
input.payloadHash,
|
|
91
|
+
].join('\n');
|
|
92
|
+
const scope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
93
|
+
const stringToSign = [ALGORITHM, amzDate, scope, sha256(canonicalRequest)].join('\n');
|
|
94
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${secretKey}`, dateStamp), region), SERVICE), 'aws4_request');
|
|
95
|
+
const signature = createHmac('sha256', signingKey).update(stringToSign, 'utf8').digest('hex');
|
|
96
|
+
return {
|
|
97
|
+
...headers,
|
|
98
|
+
authorization: `${ALGORITHM} Credential=${accessKey}/${scope}, ` +
|
|
99
|
+
`SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async #send(method, key, body, extraHeaders = {}) {
|
|
103
|
+
const url = this.#url(key);
|
|
104
|
+
const headers = this.#sign({
|
|
105
|
+
method,
|
|
106
|
+
url,
|
|
107
|
+
payloadHash: body === undefined ? UNSIGNED : sha256(body),
|
|
108
|
+
headers: body === undefined
|
|
109
|
+
? extraHeaders
|
|
110
|
+
: { ...extraHeaders, 'content-length': String(body.length) },
|
|
111
|
+
now: new Date(),
|
|
112
|
+
});
|
|
113
|
+
return fetch(url, {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
...(body === undefined ? {} : { body }),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Store an object, overwriting whatever was there. */
|
|
120
|
+
async put(key, body, contentType = 'application/octet-stream') {
|
|
121
|
+
const response = await this.#send('PUT', key, body, { 'content-type': contentType });
|
|
122
|
+
if (!response.ok)
|
|
123
|
+
throw new S3Error(response.status, 'PUT', key, await response.text());
|
|
124
|
+
}
|
|
125
|
+
/** Fetch an object. `undefined` when it is not there, which is not an error. */
|
|
126
|
+
async get(key) {
|
|
127
|
+
const response = await this.#send('GET', key);
|
|
128
|
+
if (response.status === 404)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (!response.ok)
|
|
131
|
+
throw new S3Error(response.status, 'GET', key, await response.text());
|
|
132
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Remove an object. Absent is success.
|
|
136
|
+
*
|
|
137
|
+
* S3 answers `204` whether or not the key existed, and the caller — the
|
|
138
|
+
* collector, purging a deleted document — must be able to run twice.
|
|
139
|
+
*/
|
|
140
|
+
async remove(key) {
|
|
141
|
+
const response = await this.#send('DELETE', key);
|
|
142
|
+
if (!response.ok && response.status !== 404) {
|
|
143
|
+
throw new S3Error(response.status, 'DELETE', key, await response.text());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A URL that fetches one object, for a while, without a credential.
|
|
148
|
+
*
|
|
149
|
+
* Query-string SigV4 rather than the header form: the point is a link a
|
|
150
|
+
* client can follow, so everything that authenticates it has to be in the
|
|
151
|
+
* URL. `host` is the only signed header, and the payload is unsigned because
|
|
152
|
+
* a GET has none.
|
|
153
|
+
*
|
|
154
|
+
* **This is a bearer capability and it leaves the permission model behind.**
|
|
155
|
+
* Nacre checks `read` once, when the link is minted, and the object store
|
|
156
|
+
* checks nothing afterwards — a revocation during the window does not reach a
|
|
157
|
+
* URL already handed out, and neither does a delete. That is what presigning
|
|
158
|
+
* is, in every implementation of it, and it is why the lifetime is a
|
|
159
|
+
* configured number rather than a constant: `NACRE_PRESIGN_TTL` is how long a
|
|
160
|
+
* deployment is willing for that to be true.
|
|
161
|
+
*
|
|
162
|
+
* It is also why the caller of this decides, not this: minting one per search
|
|
163
|
+
* hit would issue ten capabilities to answer a question about relevance, most
|
|
164
|
+
* never followed. See the note on the search response.
|
|
165
|
+
*/
|
|
166
|
+
presign(key, ttlSeconds) {
|
|
167
|
+
const { region, accessKey, secretKey } = this.#options;
|
|
168
|
+
const url = this.#url(key);
|
|
169
|
+
const now = new Date();
|
|
170
|
+
const amzDate = now.toISOString().replace(/[-:]|\.\d{3}/g, '');
|
|
171
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
172
|
+
const scope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
173
|
+
// Sorted by key, and encoded the same way the canonical request will be —
|
|
174
|
+
// a mismatch between the query that is signed and the query that is sent is
|
|
175
|
+
// a 403 with nothing in it about which of the two was wrong.
|
|
176
|
+
const params = [
|
|
177
|
+
['X-Amz-Algorithm', ALGORITHM],
|
|
178
|
+
['X-Amz-Credential', `${accessKey}/${scope}`],
|
|
179
|
+
['X-Amz-Date', amzDate],
|
|
180
|
+
['X-Amz-Expires', String(Math.max(1, Math.floor(ttlSeconds)))],
|
|
181
|
+
['X-Amz-SignedHeaders', 'host'],
|
|
182
|
+
];
|
|
183
|
+
const canonicalQuery = params
|
|
184
|
+
.map(([k, v]) => [encodeSegment(k), encodeSegment(v)])
|
|
185
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
186
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
187
|
+
.join('&');
|
|
188
|
+
const canonicalRequest = [
|
|
189
|
+
'GET',
|
|
190
|
+
url.pathname,
|
|
191
|
+
canonicalQuery,
|
|
192
|
+
`host:${url.host}\n`,
|
|
193
|
+
'host',
|
|
194
|
+
UNSIGNED,
|
|
195
|
+
].join('\n');
|
|
196
|
+
const stringToSign = [ALGORITHM, amzDate, scope, sha256(canonicalRequest)].join('\n');
|
|
197
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${secretKey}`, dateStamp), region), SERVICE), 'aws4_request');
|
|
198
|
+
const signature = createHmac('sha256', signingKey).update(stringToSign, 'utf8').digest('hex');
|
|
199
|
+
return `${url.origin}${url.pathname}?${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Whether the bucket answers, for readiness.
|
|
203
|
+
*
|
|
204
|
+
* A HEAD on the bucket rather than on an object: it needs no key to exist and
|
|
205
|
+
* it fails for the three reasons that matter — unreachable, wrong bucket,
|
|
206
|
+
* wrong credential. A TCP connect would pass all three.
|
|
207
|
+
*/
|
|
208
|
+
async ready() {
|
|
209
|
+
const url = this.#options.forcePathStyle
|
|
210
|
+
? new URL(`${this.#options.endpoint}/${this.#options.bucket}`)
|
|
211
|
+
: this.#url('');
|
|
212
|
+
const headers = this.#sign({
|
|
213
|
+
method: 'HEAD',
|
|
214
|
+
url,
|
|
215
|
+
payloadHash: UNSIGNED,
|
|
216
|
+
headers: {},
|
|
217
|
+
now: new Date(),
|
|
218
|
+
});
|
|
219
|
+
const response = await fetch(url, { method: 'HEAD', headers });
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
throw new S3Error(response.status, 'HEAD', this.#options.bucket, response.statusText);
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Where a document's bytes live.
|
|
228
|
+
*
|
|
229
|
+
* Derived from identity — organization, layer, external id — and never from the
|
|
230
|
+
* content. Content addressing would let two documents share an object, and then
|
|
231
|
+
* deleting one would either strand the other or need a reference count that
|
|
232
|
+
* nothing else in this system has. One document, one object, and a re-ingest
|
|
233
|
+
* overwrites in place.
|
|
234
|
+
*
|
|
235
|
+
* The external id is hashed rather than escaped: it is caller-chosen text that
|
|
236
|
+
* may contain anything, and a key derived by escaping is a key whose length and
|
|
237
|
+
* shape a caller controls.
|
|
238
|
+
*/
|
|
239
|
+
export function documentKey(orgId, layerId, externalId) {
|
|
240
|
+
return `org/${orgId}/layer/${layerId}/${sha256(externalId)}`;
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=s3.js.map
|