@substrat-run/contracts 0.87.0 → 0.89.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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Optimistic concurrency on the operation surface (#129).
3
+ *
4
+ * Two people open the same record, both save, and the second write silently
5
+ * destroys the first. Nobody notices until the data is gone — there is no error,
6
+ * no log line, and no way to reconstruct what was lost. This is the vocabulary
7
+ * that lets an operation refuse the second write instead.
8
+ *
9
+ * ## What a version is, and why it is not in here
10
+ *
11
+ * The version is the ULID of the last event about the entity (`entityVersionOf`
12
+ * in the kernel, #901). This module holds only the WIRE half — the header names
13
+ * and the comparison — because contracts sits below the spine: it can say what an
14
+ * `ETag` is called and what makes one match, and must not know which table
15
+ * answers the question.
16
+ *
17
+ * ## Why an opaque token rather than a number
18
+ *
19
+ * A caller compares; it never reads. That is what lets the version be a ULID
20
+ * today and something else later without a client noticing, and it is also what
21
+ * stops a client doing arithmetic on it — `If-Match: <version + 1>` is not a
22
+ * thing anyone can write.
23
+ *
24
+ * ## The direction this fails
25
+ *
26
+ * ANY event about the entity moves the version, including one that changed
27
+ * nothing the caller read. So a precondition built on it is CONSERVATIVE: it can
28
+ * refuse a write that would in fact have been safe, and it cannot admit one that
29
+ * would not. Documented here as well as in the kernel because this is the half a
30
+ * client author reads when they are debugging a 412 they believe is spurious.
31
+ */
32
+ /**
33
+ * The response header carrying an entity's current version.
34
+ *
35
+ * Strong, never weak (`W/"…"`): the comparison is exact, and a weak validator
36
+ * means "semantically equivalent", which is precisely the judgement no generic
37
+ * layer is entitled to make about a domain entity.
38
+ */
39
+ export declare const ETAG_HEADER = "ETag";
40
+ /** The request header carrying the version the caller believes it is writing over. */
41
+ export declare const IF_MATCH_HEADER = "If-Match";
42
+ /**
43
+ * The headers a cross-origin browser client cannot read unless the server says it
44
+ * may — the same trap already documented for `Link` and `X-Total-Count` in
45
+ * `PAGE_EXPOSED_HEADERS`, and worse here.
46
+ *
47
+ * A `Link` a browser cannot read looks like "there is only one page". An `ETag` a
48
+ * browser cannot read looks like nothing at all: `response.headers.get('ETag')` is
49
+ * `null`, the client sends no `If-Match`, the server has nothing to compare, and
50
+ * every write succeeds. The protection silently switches itself off, in the one
51
+ * deployment shape (an SPA on another origin) where two editors are most likely.
52
+ *
53
+ * So a vertical exposing its API cross-origin must expose these, and this constant
54
+ * is what it lists rather than a string it retypes.
55
+ */
56
+ export declare const CONCURRENCY_EXPOSED_HEADERS: readonly ["ETag"];
57
+ /**
58
+ * Format a version as an `ETag` value.
59
+ *
60
+ * Quoted, per RFC 9110 §8.8.3 — an entity-tag is a quoted string and a bare token
61
+ * is malformed. Intermediaries do parse this: an unquoted value is the kind of
62
+ * thing that works against a dev server and is stripped by a proxy in production.
63
+ */
64
+ export declare function etagOf(version: string): string;
65
+ /**
66
+ * Does the caller's `If-Match` admit this version?
67
+ *
68
+ * Handles the three spellings RFC 9110 §13.1.1 allows a client to send: the quoted
69
+ * tag, a comma-separated list of them, and `*` (meaning "any current version" —
70
+ * i.e. the entity must merely EXIST, which for us means it has a version at all).
71
+ *
72
+ * A weak tag (`W/"…"`) never matches. §13.1.1 requires the strong comparison
73
+ * function for `If-Match`, and honouring a weak one here would admit exactly the
74
+ * write this exists to refuse.
75
+ *
76
+ * `version` is null when the entity has no events yet. Nothing matches that except
77
+ * an absent header, which is not this function's call to make — a caller that sent
78
+ * `If-Match` against an entity with no version is asking to write over something
79
+ * that was never written, and gets a refusal.
80
+ */
81
+ export declare function ifMatchAdmits(ifMatch: string, version: string | null): boolean;
82
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,SAAS,CAAC;AAElC,sFAAsF;AACtF,eAAO,MAAM,eAAe,aAAa,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,2BAA2B,mBAAyB,CAAC;AAElE;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAQ9E"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Optimistic concurrency on the operation surface (#129).
3
+ *
4
+ * Two people open the same record, both save, and the second write silently
5
+ * destroys the first. Nobody notices until the data is gone — there is no error,
6
+ * no log line, and no way to reconstruct what was lost. This is the vocabulary
7
+ * that lets an operation refuse the second write instead.
8
+ *
9
+ * ## What a version is, and why it is not in here
10
+ *
11
+ * The version is the ULID of the last event about the entity (`entityVersionOf`
12
+ * in the kernel, #901). This module holds only the WIRE half — the header names
13
+ * and the comparison — because contracts sits below the spine: it can say what an
14
+ * `ETag` is called and what makes one match, and must not know which table
15
+ * answers the question.
16
+ *
17
+ * ## Why an opaque token rather than a number
18
+ *
19
+ * A caller compares; it never reads. That is what lets the version be a ULID
20
+ * today and something else later without a client noticing, and it is also what
21
+ * stops a client doing arithmetic on it — `If-Match: <version + 1>` is not a
22
+ * thing anyone can write.
23
+ *
24
+ * ## The direction this fails
25
+ *
26
+ * ANY event about the entity moves the version, including one that changed
27
+ * nothing the caller read. So a precondition built on it is CONSERVATIVE: it can
28
+ * refuse a write that would in fact have been safe, and it cannot admit one that
29
+ * would not. Documented here as well as in the kernel because this is the half a
30
+ * client author reads when they are debugging a 412 they believe is spurious.
31
+ */
32
+ /**
33
+ * The response header carrying an entity's current version.
34
+ *
35
+ * Strong, never weak (`W/"…"`): the comparison is exact, and a weak validator
36
+ * means "semantically equivalent", which is precisely the judgement no generic
37
+ * layer is entitled to make about a domain entity.
38
+ */
39
+ export const ETAG_HEADER = 'ETag';
40
+ /** The request header carrying the version the caller believes it is writing over. */
41
+ export const IF_MATCH_HEADER = 'If-Match';
42
+ /**
43
+ * The headers a cross-origin browser client cannot read unless the server says it
44
+ * may — the same trap already documented for `Link` and `X-Total-Count` in
45
+ * `PAGE_EXPOSED_HEADERS`, and worse here.
46
+ *
47
+ * A `Link` a browser cannot read looks like "there is only one page". An `ETag` a
48
+ * browser cannot read looks like nothing at all: `response.headers.get('ETag')` is
49
+ * `null`, the client sends no `If-Match`, the server has nothing to compare, and
50
+ * every write succeeds. The protection silently switches itself off, in the one
51
+ * deployment shape (an SPA on another origin) where two editors are most likely.
52
+ *
53
+ * So a vertical exposing its API cross-origin must expose these, and this constant
54
+ * is what it lists rather than a string it retypes.
55
+ */
56
+ export const CONCURRENCY_EXPOSED_HEADERS = [ETAG_HEADER];
57
+ /**
58
+ * Format a version as an `ETag` value.
59
+ *
60
+ * Quoted, per RFC 9110 §8.8.3 — an entity-tag is a quoted string and a bare token
61
+ * is malformed. Intermediaries do parse this: an unquoted value is the kind of
62
+ * thing that works against a dev server and is stripped by a proxy in production.
63
+ */
64
+ export function etagOf(version) {
65
+ return `"${version}"`;
66
+ }
67
+ /**
68
+ * Does the caller's `If-Match` admit this version?
69
+ *
70
+ * Handles the three spellings RFC 9110 §13.1.1 allows a client to send: the quoted
71
+ * tag, a comma-separated list of them, and `*` (meaning "any current version" —
72
+ * i.e. the entity must merely EXIST, which for us means it has a version at all).
73
+ *
74
+ * A weak tag (`W/"…"`) never matches. §13.1.1 requires the strong comparison
75
+ * function for `If-Match`, and honouring a weak one here would admit exactly the
76
+ * write this exists to refuse.
77
+ *
78
+ * `version` is null when the entity has no events yet. Nothing matches that except
79
+ * an absent header, which is not this function's call to make — a caller that sent
80
+ * `If-Match` against an entity with no version is asking to write over something
81
+ * that was never written, and gets a refusal.
82
+ */
83
+ export function ifMatchAdmits(ifMatch, version) {
84
+ const candidates = ifMatch
85
+ .split(',')
86
+ .map((raw) => raw.trim())
87
+ .filter((raw) => raw.length > 0);
88
+ if (candidates.includes('*'))
89
+ return version !== null;
90
+ if (version === null)
91
+ return false;
92
+ return candidates.includes(etagOf(version));
93
+ }
94
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;AAElC,sFAAsF;AACtF,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,WAAW,CAAU,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CAAC,OAAe;IACpC,OAAO,IAAI,OAAO,GAAG,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,OAAsB;IACnE,MAAM,UAAU,GAAG,OAAO;SACvB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;SACxB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,KAAK,IAAI,CAAC;IACtD,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The read side of the K-35 denial log — a scope-local record of every ENFORCED
4
+ * permission refusal (`assertAllowed`), written on the deny path as a fresh
5
+ * autocommit AFTER the rollback it is evidence of.
6
+ *
7
+ * This is the *other* kind of evidence from a conformance receipt. A receipt says
8
+ * "we attempted the attack in CI at commit X"; these rows say "on your data, in
9
+ * production, here is every refusal, by whom, against which key". K-35 made the
10
+ * case for the row itself: a denial is the one event where an actor's intent and
11
+ * the permission model visibly disagree.
12
+ *
13
+ * Two properties of the log shape everything below, and both come straight from
14
+ * K-35's own reasoning about why denials are NOT admin-log entries:
15
+ *
16
+ * 1. **The volume is attacker-influenceable.** A probing client mints unlimited
17
+ * rows, so a newest-first page of raw rows is the wrong default view — 200 rows
18
+ * from one prober hide everyone else. That is why `denialSummary` exists beside
19
+ * the row list, and why K-35 called rate-bucketing sanctionable up front.
20
+ * 2. **The window is a storage bound, not a retention policy.** Rows `drain` rather
21
+ * than expire (K-24's split). Until a Tier-2 sink exists, what is here is simply
22
+ * what has not been pruned — so the summary reports the window's own floor
23
+ * (`windowOldestAt`) rather than letting a caller read absence as "never happened".
24
+ */
25
+ /** How many denial rows an unbounded read returns — a screenful, newest-first. */
26
+ export declare const DEFAULT_DENIAL_LIMIT = 50;
27
+ /** The hard ceiling on one page of denial rows, and on one page of buckets. */
28
+ export declare const DENIAL_LIMIT_MAX = 200;
29
+ /**
30
+ * One recorded refusal. `scopeId` is null for a tenant-node check (one that named no
31
+ * scope); `operation` is null when the denial unwound something that was not an
32
+ * operation invocation. `drainedAt` marks a row already shipped to a Tier-2 sink and
33
+ * therefore eligible to be pruned — bookkeeping, not a judgement about the denial.
34
+ */
35
+ export declare const permissionDenial: z.ZodObject<{
36
+ id: z.ZodString;
37
+ actor: z.ZodUnion<readonly [z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">, z.ZodObject<{
38
+ system: z.core.$ZodBranded<z.ZodString, "ModuleId", "out">;
39
+ }, z.core.$strip>, z.ZodObject<{
40
+ connection: z.ZodString;
41
+ }, z.core.$strip>]>;
42
+ permission: z.core.$ZodBranded<z.ZodString, "PermissionKey", "out">;
43
+ tenantId: z.core.$ZodBranded<z.ZodString, "TenantId", "out">;
44
+ scopeId: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ScopeId", "out">>;
45
+ operation: z.ZodNullable<z.ZodString>;
46
+ at: z.ZodString;
47
+ drainedAt: z.ZodNullable<z.ZodString>;
48
+ }, z.core.$strip>;
49
+ export type PermissionDenial = z.infer<typeof permissionDenial>;
50
+ /**
51
+ * What narrows a denial read. Every field is an exact match except the `since`/`until`
52
+ * bounds on `at` (inclusive lower, exclusive upper) — enough to answer "who probed
53
+ * this key", "what did this actor try", and "what happened during the incident window"
54
+ * without a SQL console.
55
+ *
56
+ * `actor` takes the LOGICAL actor — a bare principal ULID, or the object form for a
57
+ * system/connection actor (`{"system":"invoicing"}`). The writer persists
58
+ * `JSON.stringify(actor)`, so a principal is stored with its quotes; normalizing to that
59
+ * encoding is the reader's job (`storedActor` in the kernel's query builder), not every
60
+ * caller's.
61
+ */
62
+ export declare const denialFilter: z.ZodObject<{
63
+ actor: z.ZodOptional<z.ZodString>;
64
+ permission: z.ZodOptional<z.ZodString>;
65
+ operation: z.ZodOptional<z.ZodString>;
66
+ since: z.ZodOptional<z.ZodString>;
67
+ until: z.ZodOptional<z.ZodString>;
68
+ limit: z.ZodOptional<z.ZodNumber>;
69
+ }, z.core.$strip>;
70
+ export type DenialFilter = z.infer<typeof denialFilter>;
71
+ /**
72
+ * One (actor, permission) bucket — K-35's "first occurrence + count per actor/key/
73
+ * window", which is the shape that survives a flood. `operations` is the number of
74
+ * DISTINCT operations the actor was refused this key on, and it is the discriminator
75
+ * worth the extra aggregate: one operation refused four hundred times is a broken
76
+ * screen or a misconfigured role, while the same count spread across a dozen
77
+ * operations is someone walking the surface.
78
+ */
79
+ export declare const denialBucket: z.ZodObject<{
80
+ actor: z.ZodUnion<readonly [z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">, z.ZodObject<{
81
+ system: z.core.$ZodBranded<z.ZodString, "ModuleId", "out">;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ connection: z.ZodString;
84
+ }, z.core.$strip>]>;
85
+ permission: z.core.$ZodBranded<z.ZodString, "PermissionKey", "out">;
86
+ count: z.ZodNumber;
87
+ operations: z.ZodNumber;
88
+ firstAt: z.ZodString;
89
+ lastAt: z.ZodString;
90
+ }, z.core.$strip>;
91
+ export type DenialBucket = z.infer<typeof denialBucket>;
92
+ /**
93
+ * The bucketed view of a scope's denial log, plus the facts that keep it honest.
94
+ *
95
+ * `total` counts every row matching the filter, so a caller can tell a capped bucket
96
+ * list from a complete one. `windowOldestAt` is deliberately computed WITHOUT the
97
+ * filter: it is a statement about the log, not about the query — the earliest moment
98
+ * this scope can still speak to. An empty result older than it means "no denials";
99
+ * an empty result at it means "we no longer hold that far back".
100
+ */
101
+ export declare const denialSummary: z.ZodObject<{
102
+ buckets: z.ZodArray<z.ZodObject<{
103
+ actor: z.ZodUnion<readonly [z.core.$ZodBranded<z.ZodString, "PrincipalId", "out">, z.ZodObject<{
104
+ system: z.core.$ZodBranded<z.ZodString, "ModuleId", "out">;
105
+ }, z.core.$strip>, z.ZodObject<{
106
+ connection: z.ZodString;
107
+ }, z.core.$strip>]>;
108
+ permission: z.core.$ZodBranded<z.ZodString, "PermissionKey", "out">;
109
+ count: z.ZodNumber;
110
+ operations: z.ZodNumber;
111
+ firstAt: z.ZodString;
112
+ lastAt: z.ZodString;
113
+ }, z.core.$strip>>;
114
+ total: z.ZodNumber;
115
+ actors: z.ZodNumber;
116
+ windowOldestAt: z.ZodNullable<z.ZodString>;
117
+ windowNewestAt: z.ZodNullable<z.ZodString>;
118
+ drained: z.ZodNumber;
119
+ }, z.core.$strip>;
120
+ export type DenialSummary = z.infer<typeof denialSummary>;
121
+ //# sourceMappingURL=denial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denial.d.ts","sourceRoot":"","sources":["../src/denial.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;iBAc3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY;;;;;;;iBASvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;iBAQvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;iBAgBxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
package/dist/denial.js ADDED
@@ -0,0 +1,117 @@
1
+ import { z } from 'zod';
2
+ import { actor } from './events.js';
3
+ import { permissionKey, scopeId, tenantId } from './ids.js';
4
+ /**
5
+ * The read side of the K-35 denial log — a scope-local record of every ENFORCED
6
+ * permission refusal (`assertAllowed`), written on the deny path as a fresh
7
+ * autocommit AFTER the rollback it is evidence of.
8
+ *
9
+ * This is the *other* kind of evidence from a conformance receipt. A receipt says
10
+ * "we attempted the attack in CI at commit X"; these rows say "on your data, in
11
+ * production, here is every refusal, by whom, against which key". K-35 made the
12
+ * case for the row itself: a denial is the one event where an actor's intent and
13
+ * the permission model visibly disagree.
14
+ *
15
+ * Two properties of the log shape everything below, and both come straight from
16
+ * K-35's own reasoning about why denials are NOT admin-log entries:
17
+ *
18
+ * 1. **The volume is attacker-influenceable.** A probing client mints unlimited
19
+ * rows, so a newest-first page of raw rows is the wrong default view — 200 rows
20
+ * from one prober hide everyone else. That is why `denialSummary` exists beside
21
+ * the row list, and why K-35 called rate-bucketing sanctionable up front.
22
+ * 2. **The window is a storage bound, not a retention policy.** Rows `drain` rather
23
+ * than expire (K-24's split). Until a Tier-2 sink exists, what is here is simply
24
+ * what has not been pruned — so the summary reports the window's own floor
25
+ * (`windowOldestAt`) rather than letting a caller read absence as "never happened".
26
+ */
27
+ /** How many denial rows an unbounded read returns — a screenful, newest-first. */
28
+ export const DEFAULT_DENIAL_LIMIT = 50;
29
+ /** The hard ceiling on one page of denial rows, and on one page of buckets. */
30
+ export const DENIAL_LIMIT_MAX = 200;
31
+ /**
32
+ * One recorded refusal. `scopeId` is null for a tenant-node check (one that named no
33
+ * scope); `operation` is null when the denial unwound something that was not an
34
+ * operation invocation. `drainedAt` marks a row already shipped to a Tier-2 sink and
35
+ * therefore eligible to be pruned — bookkeeping, not a judgement about the denial.
36
+ */
37
+ export const permissionDenial = z.object({
38
+ /** ULID — chronological, so it is also the sort key. */
39
+ id: z.string().min(1),
40
+ /** WHO was refused: a principal, a `{ system }` module, or a `{ connection }`. */
41
+ actor,
42
+ /** The key `assertAllowed` checked and refused. */
43
+ permission: permissionKey,
44
+ tenantId,
45
+ scopeId: scopeId.nullable(),
46
+ /** The operation the denial rolled back, e.g. `workorder/complete`. */
47
+ operation: z.string().nullable(),
48
+ /** ISO 8601. */
49
+ at: z.string().min(1),
50
+ drainedAt: z.string().nullable(),
51
+ });
52
+ /**
53
+ * What narrows a denial read. Every field is an exact match except the `since`/`until`
54
+ * bounds on `at` (inclusive lower, exclusive upper) — enough to answer "who probed
55
+ * this key", "what did this actor try", and "what happened during the incident window"
56
+ * without a SQL console.
57
+ *
58
+ * `actor` takes the LOGICAL actor — a bare principal ULID, or the object form for a
59
+ * system/connection actor (`{"system":"invoicing"}`). The writer persists
60
+ * `JSON.stringify(actor)`, so a principal is stored with its quotes; normalizing to that
61
+ * encoding is the reader's job (`storedActor` in the kernel's query builder), not every
62
+ * caller's.
63
+ */
64
+ export const denialFilter = z.object({
65
+ actor: z.string().min(1).optional(),
66
+ permission: z.string().min(1).optional(),
67
+ operation: z.string().min(1).optional(),
68
+ /** ISO 8601, inclusive. */
69
+ since: z.string().min(1).optional(),
70
+ /** ISO 8601, exclusive. */
71
+ until: z.string().min(1).optional(),
72
+ limit: z.number().int().min(1).max(DENIAL_LIMIT_MAX).optional(),
73
+ });
74
+ /**
75
+ * One (actor, permission) bucket — K-35's "first occurrence + count per actor/key/
76
+ * window", which is the shape that survives a flood. `operations` is the number of
77
+ * DISTINCT operations the actor was refused this key on, and it is the discriminator
78
+ * worth the extra aggregate: one operation refused four hundred times is a broken
79
+ * screen or a misconfigured role, while the same count spread across a dozen
80
+ * operations is someone walking the surface.
81
+ */
82
+ export const denialBucket = z.object({
83
+ actor,
84
+ permission: permissionKey,
85
+ count: z.number().int().positive(),
86
+ operations: z.number().int().nonnegative(),
87
+ /** ISO 8601 — the first occurrence still in the window (see `windowOldestAt`). */
88
+ firstAt: z.string().min(1),
89
+ lastAt: z.string().min(1),
90
+ });
91
+ /**
92
+ * The bucketed view of a scope's denial log, plus the facts that keep it honest.
93
+ *
94
+ * `total` counts every row matching the filter, so a caller can tell a capped bucket
95
+ * list from a complete one. `windowOldestAt` is deliberately computed WITHOUT the
96
+ * filter: it is a statement about the log, not about the query — the earliest moment
97
+ * this scope can still speak to. An empty result older than it means "no denials";
98
+ * an empty result at it means "we no longer hold that far back".
99
+ */
100
+ export const denialSummary = z.object({
101
+ buckets: z.array(denialBucket),
102
+ /** Rows matching the filter. Bucket counts sum to this when `buckets` is uncapped. */
103
+ total: z.number().int().nonnegative(),
104
+ /** Distinct actors among the matching rows. */
105
+ actors: z.number().int().nonnegative(),
106
+ /**
107
+ * The oldest row STILL HELD, filter ignored — the window's floor, null when the log
108
+ * is empty. The window is a storage bound and not a retention policy (K-35): what is
109
+ * absent before this instant was not necessarily never recorded.
110
+ */
111
+ windowOldestAt: z.string().nullable(),
112
+ /** The newest row held, filter ignored. */
113
+ windowNewestAt: z.string().nullable(),
114
+ /** Rows already shipped to a Tier-2 sink, filter ignored — prunable, not pruned. */
115
+ drained: z.number().int().nonnegative(),
116
+ });
117
+ //# sourceMappingURL=denial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"denial.js","sourceRoot":"","sources":["../src/denial.ts"],"names":[],"mappings":"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"}
package/dist/errors.d.ts CHANGED
@@ -33,6 +33,18 @@ import { z } from 'zod';
33
33
  * decision stays a one-line change here for exactly as long as that holds.
34
34
  */
35
35
  export declare const PROBLEM_TYPE_BASE = "https://substrat.net/errors";
36
+ /** What a problem body is served as. Never `application/json` — RFC 9457 §3. */
37
+ export declare const PROBLEM_CONTENT_TYPE = "application/problem+json";
38
+ /**
39
+ * The `type` of a failure that has a status and nothing else.
40
+ *
41
+ * RFC 9457 §4.2.1: `about:blank` means "no semantics beyond the status code", and the
42
+ * title is then the status phrase. That is the honest shape for the two cases a
43
+ * transport cannot type — an untyped throw it refuses to call the platform's fault,
44
+ * and a downstream status it is relaying — and it is what keeps the closed taxonomy
45
+ * closed while every body still parses as a problem.
46
+ */
47
+ export declare const PROBLEM_TYPE_BLANK = "about:blank";
36
48
  /**
37
49
  * The taxonomy. CLOSED — an open one is a suggestion.
38
50
  *
@@ -140,7 +152,12 @@ export declare const PROBLEM_EXTENSIONS: {
140
152
  message: z.ZodString;
141
153
  }, z.core.$strip>>>;
142
154
  }, z.core.$strip>;
143
- readonly precondition_failed: z.ZodObject<{}, z.core.$strict>;
155
+ readonly precondition_failed: z.ZodObject<{
156
+ entity: z.ZodOptional<z.ZodObject<{
157
+ entityType: z.ZodString;
158
+ entityId: z.ZodString;
159
+ }, z.core.$strip>>;
160
+ }, z.core.$strip>;
144
161
  readonly rate_limited: z.ZodObject<{
145
162
  retryAfter: z.ZodOptional<z.ZodNumber>;
146
163
  }, z.core.$strip>;
@@ -163,7 +180,7 @@ export declare const problem: z.ZodObject<{
163
180
  detail: z.ZodOptional<z.ZodString>;
164
181
  instance: z.ZodOptional<z.ZodString>;
165
182
  error: z.ZodOptional<z.ZodString>;
166
- code: z.ZodEnum<{
183
+ code: z.ZodOptional<z.ZodEnum<{
167
184
  conflict: "conflict";
168
185
  forbidden: "forbidden";
169
186
  internal: "internal";
@@ -174,7 +191,7 @@ export declare const problem: z.ZodObject<{
174
191
  unauthenticated: "unauthenticated";
175
192
  unavailable: "unavailable";
176
193
  validation_failed: "validation_failed";
177
- }>;
194
+ }>>;
178
195
  permission: z.ZodOptional<z.ZodString>;
179
196
  entity: z.ZodOptional<z.ZodObject<{
180
197
  entityType: z.ZodString;
@@ -248,6 +265,26 @@ export declare function validationIssuesFrom(error: z.ZodError): ValidationIssue
248
265
  * quietly widening it in the name of better errors.
249
266
  */
250
267
  export declare function toProblem(err: unknown, instance?: string): Problem;
268
+ /**
269
+ * A problem body for a status and nothing else — the `about:blank` form.
270
+ *
271
+ * Two callers, both transports, both relaying rather than raising:
272
+ *
273
+ * - **A throw the taxonomy does not recognise.** Every vertical answers one with the
274
+ * caller's 400 and relays the message, deliberately (#559: an unrecognised throw must
275
+ * not claim to be the platform's fault, because the control plane retries 5xx). That
276
+ * status is a decision about blame, not a claim about what went wrong, and this is the
277
+ * body that says so.
278
+ * - **A status raised somewhere else.** A downstream vertical's own refusal, a Durable
279
+ * Object fault the runtime named (502). Inventing a code for those would put our
280
+ * vocabulary on someone else's failure.
281
+ *
282
+ * `detail` is carried as the caller passes it. That is safe here and not in `toProblem`
283
+ * because a caller of THIS function has a status it chose or received, which means it
284
+ * has already looked at what it is relaying; `toProblem`'s `internal` branch is the one
285
+ * holding an unreviewed message, and it still refuses to disclose it.
286
+ */
287
+ export declare function problemForStatus(status: number, detail?: string, instance?: string): Problem;
251
288
  /**
252
289
  * The statuses an operation can actually answer with today, for the emitted document.
253
290
  *
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,gCAAgC,CAAC;AAE/D;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;EAWpB,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAElD,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEtD;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe;;iBACP,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,iBAAiB;;;iBACnC,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,mBAAmB;;;iBAC/C,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,WAAW;;;iBAC/B,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,WAAW;;;iBAChC,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,UAAU;;;iBACrB,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,mBAAmB;;;iBACrC,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,qBAAqB;;;iBAChD,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,cAAc;;;iBACnC,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,qBAAqB;;;iBAC5C,MAAM,EAAE,GAAG;iBAAE,KAAK,EAAE,gBAAgB;;CACuB,CAAC;AAE1E,0DAA0D;AAC1D,eAAO,MAAM,eAAe;;;iBAI1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgBkB,CAAC;AAElD,kFAAkF;AAClF,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzF;;;;;;GAMG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BlB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAkB7C;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAEvD,YAAY,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAMrF;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAgB/D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,EAC/C,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAC5B,aAAa,CAGf;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,aAAa,CAElE;AAED,qDAAqD;AACrD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,CAAC,CAAC,QAAQ,GAAG,eAAe,EAAE,CAKzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAelE;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,SAAS,EAStD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;iBAOtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAUvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,KAAK,CAO3D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}