@greatwait/contracts 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authority.d.ts +217 -0
- package/dist/authority.js +261 -0
- package/dist/callback-ownership.d.ts +58 -0
- package/dist/callback-ownership.js +71 -0
- package/dist/callback-safety.d.ts +38 -0
- package/dist/callback-safety.js +403 -0
- package/dist/compatibility.d.ts +82 -0
- package/dist/compatibility.js +115 -0
- package/dist/delivery.d.ts +401 -0
- package/dist/delivery.js +311 -0
- package/dist/endpoints.d.ts +305 -0
- package/dist/endpoints.js +179 -0
- package/dist/events.d.ts +62 -0
- package/dist/events.js +38 -0
- package/dist/first-value-journey.d.ts +305 -0
- package/dist/first-value-journey.js +466 -0
- package/dist/forecast.d.ts +443 -0
- package/dist/forecast.js +729 -0
- package/dist/identifiers.d.ts +109 -0
- package/dist/identifiers.js +131 -0
- package/dist/identity.d.ts +196 -0
- package/dist/identity.js +182 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mcp.d.ts +51 -0
- package/dist/mcp.js +56 -0
- package/dist/oauth.d.ts +796 -0
- package/dist/oauth.js +708 -0
- package/dist/operations.d.ts +3985 -0
- package/dist/operations.js +1837 -0
- package/dist/partition.d.ts +771 -0
- package/dist/partition.js +206 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +180 -0
- package/dist/problems.d.ts +158 -0
- package/dist/problems.js +104 -0
- package/dist/protocol.d.ts +308 -0
- package/dist/protocol.js +515 -0
- package/dist/scheduling-records.d.ts +395 -0
- package/dist/scheduling-records.js +284 -0
- package/dist/scheduling.d.ts +1098 -0
- package/dist/scheduling.js +1116 -0
- package/dist/secrets.d.ts +383 -0
- package/dist/secrets.js +272 -0
- package/dist/signatures.d.ts +704 -0
- package/dist/signatures.js +867 -0
- package/dist/tenancy.d.ts +178 -0
- package/dist/tenancy.js +145 -0
- package/dist/test-sink.d.ts +986 -0
- package/dist/test-sink.js +755 -0
- package/dist/time.d.ts +45 -0
- package/dist/time.js +128 -0
- package/dist/usage.d.ts +121 -0
- package/dist/usage.js +200 -0
- package/dist/webhooks.d.ts +116 -0
- package/dist/webhooks.js +161 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.js +59 -0
- package/generated/v1/callback-delivery-signature.json +467 -0
- package/generated/v1/callback-endpoint-verification.json +59 -0
- package/generated/v1/callback-receiver.json +241 -0
- package/generated/v1/examples/attempts.list.response.json +24 -0
- package/generated/v1/examples/attempts.read.response.json +20 -0
- package/generated/v1/examples/credentials.create.request.json +5 -0
- package/generated/v1/examples/credentials.create.response.json +10 -0
- package/generated/v1/examples/credentials.list.response.json +17 -0
- package/generated/v1/examples/credentials.revoke.response.json +11 -0
- package/generated/v1/examples/credentials.rotate.request.json +3 -0
- package/generated/v1/examples/credentials.rotate.response.json +22 -0
- package/generated/v1/examples/endpoints.create.request.json +3 -0
- package/generated/v1/examples/endpoints.create.response.json +16 -0
- package/generated/v1/examples/endpoints.disable.response.json +15 -0
- package/generated/v1/examples/endpoints.list.response.json +21 -0
- package/generated/v1/examples/endpoints.read.response.json +14 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
- package/generated/v1/examples/endpoints.update.request.json +3 -0
- package/generated/v1/examples/endpoints.update.response.json +13 -0
- package/generated/v1/examples/endpoints.verify.response.json +15 -0
- package/generated/v1/examples/forecast.read.response.json +28 -0
- package/generated/v1/examples/forecast.windows.response.json +21 -0
- package/generated/v1/examples/grants.list.response.json +17 -0
- package/generated/v1/examples/grants.revoke.response.json +16 -0
- package/generated/v1/examples/meta.scaffold.response.json +5 -0
- package/generated/v1/examples/meta.version.response.json +5 -0
- package/generated/v1/examples/occurrences.list.response.json +17 -0
- package/generated/v1/examples/occurrences.read.response.json +15 -0
- package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
- package/generated/v1/examples/projects.create.request.json +3 -0
- package/generated/v1/examples/projects.create.response.json +5 -0
- package/generated/v1/examples/projects.list.response.json +9 -0
- package/generated/v1/examples/schedules.archive.response.json +25 -0
- package/generated/v1/examples/schedules.cancel.response.json +24 -0
- package/generated/v1/examples/schedules.create.request.json +9 -0
- package/generated/v1/examples/schedules.create.response.json +25 -0
- package/generated/v1/examples/schedules.list.response.json +23 -0
- package/generated/v1/examples/schedules.pause.response.json +23 -0
- package/generated/v1/examples/schedules.read.response.json +25 -0
- package/generated/v1/examples/schedules.replace.request.json +8 -0
- package/generated/v1/examples/schedules.replace.response.json +25 -0
- package/generated/v1/examples/schedules.resume.response.json +23 -0
- package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
- package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
- package/generated/v1/examples/testsinks.configure.request.json +3 -0
- package/generated/v1/examples/testsinks.configure.response.json +13 -0
- package/generated/v1/examples/testsinks.create.request.json +8 -0
- package/generated/v1/examples/testsinks.create.response.json +13 -0
- package/generated/v1/examples/testsinks.delete.response.json +13 -0
- package/generated/v1/examples/testsinks.list.response.json +20 -0
- package/generated/v1/examples/testsinks.read.response.json +14 -0
- package/generated/v1/examples/usage.read.response.json +20 -0
- package/generated/v1/first-value-journey.json +436 -0
- package/generated/v1/manifest.json +1037 -0
- package/generated/v1/openapi.json +23665 -0
- package/generated/v1/policy-matrix.json +305 -0
- package/generated/v1/schemas/attempts.list.query.json +16 -0
- package/generated/v1/schemas/attempts.list.response.json +175 -0
- package/generated/v1/schemas/attempts.read.response.json +168 -0
- package/generated/v1/schemas/credentials.create.request.json +39 -0
- package/generated/v1/schemas/credentials.create.response.json +88 -0
- package/generated/v1/schemas/credentials.list.query.json +16 -0
- package/generated/v1/schemas/credentials.list.response.json +124 -0
- package/generated/v1/schemas/credentials.revoke.response.json +84 -0
- package/generated/v1/schemas/credentials.rotate.request.json +11 -0
- package/generated/v1/schemas/credentials.rotate.response.json +171 -0
- package/generated/v1/schemas/endpoints.create.request.json +12 -0
- package/generated/v1/schemas/endpoints.create.response.json +115 -0
- package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.disable.response.json +100 -0
- package/generated/v1/schemas/endpoints.list.query.json +16 -0
- package/generated/v1/schemas/endpoints.list.response.json +146 -0
- package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.read.response.json +100 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
- package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.update.request.json +12 -0
- package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.update.response.json +100 -0
- package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.verify.response.json +100 -0
- package/generated/v1/schemas/forecast.read.query.json +26 -0
- package/generated/v1/schemas/forecast.read.response.json +100 -0
- package/generated/v1/schemas/forecast.windows.query.json +26 -0
- package/generated/v1/schemas/forecast.windows.response.json +94 -0
- package/generated/v1/schemas/grants.list.response.json +98 -0
- package/generated/v1/schemas/grants.revoke.response.json +95 -0
- package/generated/v1/schemas/meta.scaffold.response.json +36 -0
- package/generated/v1/schemas/meta.version.response.json +19 -0
- package/generated/v1/schemas/occurrences.list.query.json +16 -0
- package/generated/v1/schemas/occurrences.list.response.json +83 -0
- package/generated/v1/schemas/occurrences.read.response.json +76 -0
- package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
- package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
- package/generated/v1/schemas/projects.create.request.json +12 -0
- package/generated/v1/schemas/projects.create.response.json +25 -0
- package/generated/v1/schemas/projects.list.response.json +35 -0
- package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.archive.response.json +189 -0
- package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.cancel.response.json +189 -0
- package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
- package/generated/v1/schemas/schedules.create.request.json +99 -0
- package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
- package/generated/v1/schemas/schedules.create.response.json +189 -0
- package/generated/v1/schemas/schedules.list.query.json +16 -0
- package/generated/v1/schemas/schedules.list.response.json +170 -0
- package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.pause.response.json +189 -0
- package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.read.response.json +189 -0
- package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.replace.request.json +99 -0
- package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.replace.response.json +189 -0
- package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.resume.response.json +189 -0
- package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
- package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
- package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
- package/generated/v1/schemas/testsinks.configure.request.json +66 -0
- package/generated/v1/schemas/testsinks.configure.response.json +115 -0
- package/generated/v1/schemas/testsinks.create.request.json +76 -0
- package/generated/v1/schemas/testsinks.create.response.json +115 -0
- package/generated/v1/schemas/testsinks.delete.response.json +115 -0
- package/generated/v1/schemas/testsinks.list.query.json +16 -0
- package/generated/v1/schemas/testsinks.list.response.json +161 -0
- package/generated/v1/schemas/testsinks.read.response.json +115 -0
- package/generated/v1/schemas/usage.read.query.json +15 -0
- package/generated/v1/schemas/usage.read.response.json +71 -0
- package/generated/v1/snippets/attempts.list.json +5 -0
- package/generated/v1/snippets/attempts.read.json +5 -0
- package/generated/v1/snippets/credentials.create.json +5 -0
- package/generated/v1/snippets/credentials.list.json +5 -0
- package/generated/v1/snippets/credentials.revoke.json +5 -0
- package/generated/v1/snippets/credentials.rotate.json +5 -0
- package/generated/v1/snippets/endpoints.create.json +5 -0
- package/generated/v1/snippets/endpoints.disable.json +5 -0
- package/generated/v1/snippets/endpoints.list.json +5 -0
- package/generated/v1/snippets/endpoints.read.json +5 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
- package/generated/v1/snippets/endpoints.update.json +5 -0
- package/generated/v1/snippets/endpoints.verify.json +5 -0
- package/generated/v1/snippets/forecast.read.json +5 -0
- package/generated/v1/snippets/forecast.windows.json +5 -0
- package/generated/v1/snippets/grants.list.json +5 -0
- package/generated/v1/snippets/grants.revoke.json +5 -0
- package/generated/v1/snippets/meta.scaffold.json +5 -0
- package/generated/v1/snippets/meta.version.json +5 -0
- package/generated/v1/snippets/occurrences.list.json +5 -0
- package/generated/v1/snippets/occurrences.read.json +5 -0
- package/generated/v1/snippets/occurrences.redeliver.json +5 -0
- package/generated/v1/snippets/projects.create.json +5 -0
- package/generated/v1/snippets/projects.list.json +5 -0
- package/generated/v1/snippets/schedules.archive.json +5 -0
- package/generated/v1/snippets/schedules.cancel.json +5 -0
- package/generated/v1/snippets/schedules.create.json +5 -0
- package/generated/v1/snippets/schedules.list.json +5 -0
- package/generated/v1/snippets/schedules.pause.json +5 -0
- package/generated/v1/snippets/schedules.read.json +5 -0
- package/generated/v1/snippets/schedules.replace.json +5 -0
- package/generated/v1/snippets/schedules.resume.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
- package/generated/v1/snippets/testsinks.configure.json +5 -0
- package/generated/v1/snippets/testsinks.create.json +5 -0
- package/generated/v1/snippets/testsinks.delete.json +5 -0
- package/generated/v1/snippets/testsinks.list.json +5 -0
- package/generated/v1/snippets/testsinks.read.json +5 -0
- package/generated/v1/snippets/usage.read.json +5 -0
- package/package.json +153 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Secret handling and redaction.
|
|
4
|
+
*
|
|
5
|
+
* A secret is shown exactly once, at creation. After that only its non-secret
|
|
6
|
+
* prefix and metadata are readable — by the API, by MCP tools, by the
|
|
7
|
+
* dashboard, and by support. There is no "reveal" endpoint, because a store
|
|
8
|
+
* that can reveal a secret is a store that can leak one.
|
|
9
|
+
*/
|
|
10
|
+
/** `pt_` + environment + `_` + public prefix. The secret half is never stored. */
|
|
11
|
+
export declare const CREDENTIAL_PREFIX_PATTERN: RegExp;
|
|
12
|
+
/** How many characters of a credential are the non-secret display prefix. */
|
|
13
|
+
export declare const CREDENTIAL_PREFIX_LENGTH = 16;
|
|
14
|
+
/** How many characters of high-entropy secret follow the display prefix. */
|
|
15
|
+
export declare const CREDENTIAL_SECRET_LENGTH = 32;
|
|
16
|
+
/**
|
|
17
|
+
* A whole credential: the display prefix, followed immediately by the secret
|
|
18
|
+
* half. `pt_test_a1b2c3d4` is the prefix of `pt_test_a1b2c3d4<32 more>`, which
|
|
19
|
+
* is what lets a presented credential be looked up by prefix before anything is
|
|
20
|
+
* compared — a store that had to scan every row to find a candidate would leak
|
|
21
|
+
* timing, and a separator would be one more thing for a client to get wrong.
|
|
22
|
+
*
|
|
23
|
+
* **This pattern is enforced where credentials are issued and verified, and
|
|
24
|
+
* deliberately not by `credentialCreatedSchema` below.** A response schema is
|
|
25
|
+
* projected into a committed example, and an example matching this pattern is
|
|
26
|
+
* indistinguishable from a real credential to `npm run check:secrets`, which
|
|
27
|
+
* would refuse it — correctly. So the wire schema states the length floor, the
|
|
28
|
+
* issuer states the format, and the published example is visibly a placeholder.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CREDENTIAL_SECRET_PATTERN: RegExp;
|
|
31
|
+
/**
|
|
32
|
+
* A credential as any read shape shows it: what it can do, where it can do it,
|
|
33
|
+
* and enough to recognise which one it is. Never the secret.
|
|
34
|
+
*/
|
|
35
|
+
export declare const credentialMetadataSchema: z.ZodObject<{
|
|
36
|
+
createdAt: z.ZodString;
|
|
37
|
+
displayPrefix: z.ZodString;
|
|
38
|
+
environmentKind: z.ZodEnum<{
|
|
39
|
+
test: "test";
|
|
40
|
+
live: "live";
|
|
41
|
+
}>;
|
|
42
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
43
|
+
id: z.ZodString;
|
|
44
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
45
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
46
|
+
project: z.ZodString;
|
|
47
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
48
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
49
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
50
|
+
"credentials:administer": "credentials:administer";
|
|
51
|
+
"credentials:read": "credentials:read";
|
|
52
|
+
"credentials:write": "credentials:write";
|
|
53
|
+
"endpoints:read": "endpoints:read";
|
|
54
|
+
"endpoints:write": "endpoints:write";
|
|
55
|
+
"forecast:read": "forecast:read";
|
|
56
|
+
"grants:read": "grants:read";
|
|
57
|
+
"grants:write": "grants:write";
|
|
58
|
+
"occurrences:read": "occurrences:read";
|
|
59
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
60
|
+
"projects:read": "projects:read";
|
|
61
|
+
"projects:write": "projects:write";
|
|
62
|
+
"schedules:read": "schedules:read";
|
|
63
|
+
"schedules:write": "schedules:write";
|
|
64
|
+
"usage:read": "usage:read";
|
|
65
|
+
}>>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
export type CredentialMetadata = z.infer<typeof credentialMetadataSchema>;
|
|
68
|
+
/**
|
|
69
|
+
* A page of credentials.
|
|
70
|
+
*
|
|
71
|
+
* Built from `pageSchema` rather than written out, so paging is defined once:
|
|
72
|
+
* the cursor, the bounded limit and the freshness discriminator on this
|
|
73
|
+
* collection are the same ones every later collection gets, and a client that
|
|
74
|
+
* can page one can page all of them.
|
|
75
|
+
*
|
|
76
|
+
* The item is `credentialMetadataSchema` — the read shape — so there is no
|
|
77
|
+
* shape in the contract in which a list could carry a secret. That is a
|
|
78
|
+
* property of the type rather than of the query behind it.
|
|
79
|
+
*/
|
|
80
|
+
export declare const credentialPageSchema: z.ZodObject<{
|
|
81
|
+
data: z.ZodArray<z.ZodObject<{
|
|
82
|
+
createdAt: z.ZodString;
|
|
83
|
+
displayPrefix: z.ZodString;
|
|
84
|
+
environmentKind: z.ZodEnum<{
|
|
85
|
+
test: "test";
|
|
86
|
+
live: "live";
|
|
87
|
+
}>;
|
|
88
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
89
|
+
id: z.ZodString;
|
|
90
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
91
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
92
|
+
project: z.ZodString;
|
|
93
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
94
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
95
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
96
|
+
"credentials:administer": "credentials:administer";
|
|
97
|
+
"credentials:read": "credentials:read";
|
|
98
|
+
"credentials:write": "credentials:write";
|
|
99
|
+
"endpoints:read": "endpoints:read";
|
|
100
|
+
"endpoints:write": "endpoints:write";
|
|
101
|
+
"forecast:read": "forecast:read";
|
|
102
|
+
"grants:read": "grants:read";
|
|
103
|
+
"grants:write": "grants:write";
|
|
104
|
+
"occurrences:read": "occurrences:read";
|
|
105
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
106
|
+
"projects:read": "projects:read";
|
|
107
|
+
"projects:write": "projects:write";
|
|
108
|
+
"schedules:read": "schedules:read";
|
|
109
|
+
"schedules:write": "schedules:write";
|
|
110
|
+
"usage:read": "usage:read";
|
|
111
|
+
}>>;
|
|
112
|
+
}, z.core.$strip>>;
|
|
113
|
+
freshness: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
114
|
+
kind: z.ZodLiteral<"authoritative">;
|
|
115
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
116
|
+
asOf: z.ZodString;
|
|
117
|
+
kind: z.ZodLiteral<"projected">;
|
|
118
|
+
lagSeconds: z.ZodNumber;
|
|
119
|
+
}, z.core.$strip>], "kind">;
|
|
120
|
+
nextCursor: z.ZodOptional<z.ZodString>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
export type CredentialPage = z.infer<typeof credentialPageSchema>;
|
|
123
|
+
/**
|
|
124
|
+
* The one-time creation response. `secret` is present here and nowhere else in
|
|
125
|
+
* the contract; every read shape carries metadata only.
|
|
126
|
+
*/
|
|
127
|
+
export declare const credentialCreatedSchema: z.ZodObject<{
|
|
128
|
+
createdAt: z.ZodString;
|
|
129
|
+
displayPrefix: z.ZodString;
|
|
130
|
+
environmentKind: z.ZodEnum<{
|
|
131
|
+
test: "test";
|
|
132
|
+
live: "live";
|
|
133
|
+
}>;
|
|
134
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
135
|
+
id: z.ZodString;
|
|
136
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
137
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
138
|
+
project: z.ZodString;
|
|
139
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
140
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
141
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
142
|
+
"credentials:administer": "credentials:administer";
|
|
143
|
+
"credentials:read": "credentials:read";
|
|
144
|
+
"credentials:write": "credentials:write";
|
|
145
|
+
"endpoints:read": "endpoints:read";
|
|
146
|
+
"endpoints:write": "endpoints:write";
|
|
147
|
+
"forecast:read": "forecast:read";
|
|
148
|
+
"grants:read": "grants:read";
|
|
149
|
+
"grants:write": "grants:write";
|
|
150
|
+
"occurrences:read": "occurrences:read";
|
|
151
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
152
|
+
"projects:read": "projects:read";
|
|
153
|
+
"projects:write": "projects:write";
|
|
154
|
+
"schedules:read": "schedules:read";
|
|
155
|
+
"schedules:write": "schedules:write";
|
|
156
|
+
"usage:read": "usage:read";
|
|
157
|
+
}>>;
|
|
158
|
+
secret: z.ZodString;
|
|
159
|
+
shownOnce: z.ZodLiteral<true>;
|
|
160
|
+
}, z.core.$strip>;
|
|
161
|
+
export type CredentialCreated = z.infer<typeof credentialCreatedSchema>;
|
|
162
|
+
/**
|
|
163
|
+
* What a caller asks for when issuing a credential.
|
|
164
|
+
*
|
|
165
|
+
* `project` and `environmentKind` are caller-supplied and then *validated
|
|
166
|
+
* against the principal's organization* — they select within the caller's own
|
|
167
|
+
* tenancy rather than widening it, which is the same shape `resolveAuthority`
|
|
168
|
+
* already has. The organization is never here: that would be selecting a
|
|
169
|
+
* tenant, which `docs/contracts/v1-conventions.md` forbids outright.
|
|
170
|
+
*/
|
|
171
|
+
export declare const credentialRequestSchema: z.ZodObject<{
|
|
172
|
+
environmentKind: z.ZodEnum<{
|
|
173
|
+
test: "test";
|
|
174
|
+
live: "live";
|
|
175
|
+
}>;
|
|
176
|
+
project: z.ZodString;
|
|
177
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
178
|
+
"credentials:administer": "credentials:administer";
|
|
179
|
+
"credentials:read": "credentials:read";
|
|
180
|
+
"credentials:write": "credentials:write";
|
|
181
|
+
"endpoints:read": "endpoints:read";
|
|
182
|
+
"endpoints:write": "endpoints:write";
|
|
183
|
+
"forecast:read": "forecast:read";
|
|
184
|
+
"grants:read": "grants:read";
|
|
185
|
+
"grants:write": "grants:write";
|
|
186
|
+
"occurrences:read": "occurrences:read";
|
|
187
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
188
|
+
"projects:read": "projects:read";
|
|
189
|
+
"projects:write": "projects:write";
|
|
190
|
+
"schedules:read": "schedules:read";
|
|
191
|
+
"schedules:write": "schedules:write";
|
|
192
|
+
"usage:read": "usage:read";
|
|
193
|
+
}>>;
|
|
194
|
+
}, z.core.$strip>;
|
|
195
|
+
export type CredentialRequest = z.infer<typeof credentialRequestSchema>;
|
|
196
|
+
/**
|
|
197
|
+
* The longest a rotated credential may keep working, and its default.
|
|
198
|
+
*
|
|
199
|
+
* The number is a judgement rather than a derivation: long enough that a deploy
|
|
200
|
+
* can roll a new secret through every consumer, short enough that a leaked old
|
|
201
|
+
* secret stops being useful within a day. It is stated here rather than left in
|
|
202
|
+
* a route so that changing it is a contract change somebody reviews.
|
|
203
|
+
*/
|
|
204
|
+
export declare const CREDENTIAL_ROTATION_OVERLAP_SECONDS = 86400;
|
|
205
|
+
/**
|
|
206
|
+
* What a caller asks for when rotating.
|
|
207
|
+
*
|
|
208
|
+
* `overlapSeconds` may only *shorten* the window — the default is also the
|
|
209
|
+
* maximum. A caller who knows their consumers have already picked up the new
|
|
210
|
+
* secret can retire the old one immediately; nobody can ask for a longer tail
|
|
211
|
+
* than the contract allows, because the tail is the part that is dangerous.
|
|
212
|
+
*
|
|
213
|
+
* Zero is permitted and means the old credential stops verifying at once, which
|
|
214
|
+
* is deliberately not the same as revoking it: the audit trail records a
|
|
215
|
+
* rotation, and the retired credential names its replacement.
|
|
216
|
+
*/
|
|
217
|
+
export declare const credentialRotationRequestSchema: z.ZodObject<{
|
|
218
|
+
overlapSeconds: z.ZodOptional<z.ZodNumber>;
|
|
219
|
+
}, z.core.$strip>;
|
|
220
|
+
export type CredentialRotationRequest = z.infer<typeof credentialRotationRequestSchema>;
|
|
221
|
+
/**
|
|
222
|
+
* The rotation response: the replacement, and what became of the original.
|
|
223
|
+
*
|
|
224
|
+
* Both halves are here because a caller that only learned the new secret would
|
|
225
|
+
* have no way to know when the old one stops working, and the deadline is the
|
|
226
|
+
* whole point of an overlap. `retired` is metadata only — rotation never
|
|
227
|
+
* reveals a secret it did not just mint.
|
|
228
|
+
*/
|
|
229
|
+
export declare const credentialRotatedSchema: z.ZodObject<{
|
|
230
|
+
credential: z.ZodObject<{
|
|
231
|
+
createdAt: z.ZodString;
|
|
232
|
+
displayPrefix: z.ZodString;
|
|
233
|
+
environmentKind: z.ZodEnum<{
|
|
234
|
+
test: "test";
|
|
235
|
+
live: "live";
|
|
236
|
+
}>;
|
|
237
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
238
|
+
id: z.ZodString;
|
|
239
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
240
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
241
|
+
project: z.ZodString;
|
|
242
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
243
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
244
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
245
|
+
"credentials:administer": "credentials:administer";
|
|
246
|
+
"credentials:read": "credentials:read";
|
|
247
|
+
"credentials:write": "credentials:write";
|
|
248
|
+
"endpoints:read": "endpoints:read";
|
|
249
|
+
"endpoints:write": "endpoints:write";
|
|
250
|
+
"forecast:read": "forecast:read";
|
|
251
|
+
"grants:read": "grants:read";
|
|
252
|
+
"grants:write": "grants:write";
|
|
253
|
+
"occurrences:read": "occurrences:read";
|
|
254
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
255
|
+
"projects:read": "projects:read";
|
|
256
|
+
"projects:write": "projects:write";
|
|
257
|
+
"schedules:read": "schedules:read";
|
|
258
|
+
"schedules:write": "schedules:write";
|
|
259
|
+
"usage:read": "usage:read";
|
|
260
|
+
}>>;
|
|
261
|
+
secret: z.ZodString;
|
|
262
|
+
shownOnce: z.ZodLiteral<true>;
|
|
263
|
+
}, z.core.$strip>;
|
|
264
|
+
retired: z.ZodObject<{
|
|
265
|
+
createdAt: z.ZodString;
|
|
266
|
+
displayPrefix: z.ZodString;
|
|
267
|
+
environmentKind: z.ZodEnum<{
|
|
268
|
+
test: "test";
|
|
269
|
+
live: "live";
|
|
270
|
+
}>;
|
|
271
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
272
|
+
id: z.ZodString;
|
|
273
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
274
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
275
|
+
project: z.ZodString;
|
|
276
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
277
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
278
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
279
|
+
"credentials:administer": "credentials:administer";
|
|
280
|
+
"credentials:read": "credentials:read";
|
|
281
|
+
"credentials:write": "credentials:write";
|
|
282
|
+
"endpoints:read": "endpoints:read";
|
|
283
|
+
"endpoints:write": "endpoints:write";
|
|
284
|
+
"forecast:read": "forecast:read";
|
|
285
|
+
"grants:read": "grants:read";
|
|
286
|
+
"grants:write": "grants:write";
|
|
287
|
+
"occurrences:read": "occurrences:read";
|
|
288
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
289
|
+
"projects:read": "projects:read";
|
|
290
|
+
"projects:write": "projects:write";
|
|
291
|
+
"schedules:read": "schedules:read";
|
|
292
|
+
"schedules:write": "schedules:write";
|
|
293
|
+
"usage:read": "usage:read";
|
|
294
|
+
}>>;
|
|
295
|
+
}, z.core.$strip>;
|
|
296
|
+
}, z.core.$strip>;
|
|
297
|
+
export type CredentialRotated = z.infer<typeof credentialRotatedSchema>;
|
|
298
|
+
/**
|
|
299
|
+
* The revocation response.
|
|
300
|
+
*
|
|
301
|
+
* Metadata rather than an empty body, because the contract has no
|
|
302
|
+
* representation for one — `successStatusFor` yields 200 or 201 and every
|
|
303
|
+
* operation must declare a response schema. That constraint turns out to be the
|
|
304
|
+
* better answer anyway: returning the revoked credential lets a caller confirm
|
|
305
|
+
* *which* credential stopped working and when, which an empty 204 would make
|
|
306
|
+
* them ask for separately.
|
|
307
|
+
*/
|
|
308
|
+
export declare const credentialRevokedSchema: z.ZodObject<{
|
|
309
|
+
credential: z.ZodObject<{
|
|
310
|
+
createdAt: z.ZodString;
|
|
311
|
+
displayPrefix: z.ZodString;
|
|
312
|
+
environmentKind: z.ZodEnum<{
|
|
313
|
+
test: "test";
|
|
314
|
+
live: "live";
|
|
315
|
+
}>;
|
|
316
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
317
|
+
id: z.ZodString;
|
|
318
|
+
issuedBy: z.ZodOptional<z.ZodString>;
|
|
319
|
+
lastUsedAt: z.ZodOptional<z.ZodString>;
|
|
320
|
+
project: z.ZodString;
|
|
321
|
+
revokedAt: z.ZodOptional<z.ZodString>;
|
|
322
|
+
supersededBy: z.ZodOptional<z.ZodString>;
|
|
323
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
324
|
+
"credentials:administer": "credentials:administer";
|
|
325
|
+
"credentials:read": "credentials:read";
|
|
326
|
+
"credentials:write": "credentials:write";
|
|
327
|
+
"endpoints:read": "endpoints:read";
|
|
328
|
+
"endpoints:write": "endpoints:write";
|
|
329
|
+
"forecast:read": "forecast:read";
|
|
330
|
+
"grants:read": "grants:read";
|
|
331
|
+
"grants:write": "grants:write";
|
|
332
|
+
"occurrences:read": "occurrences:read";
|
|
333
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
334
|
+
"projects:read": "projects:read";
|
|
335
|
+
"projects:write": "projects:write";
|
|
336
|
+
"schedules:read": "schedules:read";
|
|
337
|
+
"schedules:write": "schedules:write";
|
|
338
|
+
"usage:read": "usage:read";
|
|
339
|
+
}>>;
|
|
340
|
+
}, z.core.$strip>;
|
|
341
|
+
}, z.core.$strip>;
|
|
342
|
+
export type CredentialRevokedResponse = z.infer<typeof credentialRevokedSchema>;
|
|
343
|
+
/**
|
|
344
|
+
* Field-name fragments whose values must never reach a log, metric, or support
|
|
345
|
+
* index.
|
|
346
|
+
*
|
|
347
|
+
* Matched as substrings against a normalized key rather than by exact name.
|
|
348
|
+
* Exact matching caught `token` and missed `refreshToken`, `access_token`,
|
|
349
|
+
* `apiKey`, `client_secret` and `password` — which is most of what
|
|
350
|
+
* architecture section 10 actually requires be kept out of logs. Naming
|
|
351
|
+
* conventions vary across the codebase and across the libraries we log from,
|
|
352
|
+
* so the rule has to be insensitive to them.
|
|
353
|
+
*/
|
|
354
|
+
export declare const REDACTED_FIELD_FRAGMENTS: readonly ["authorization", "cookie", "credential", "key", "password", "secret", "signature", "token"];
|
|
355
|
+
/**
|
|
356
|
+
* Fields redacted wholesale because they are content, not metadata.
|
|
357
|
+
*
|
|
358
|
+
* Architecture section 10 names callback bodies and prompts alongside
|
|
359
|
+
* payloads. Matched exactly rather than as fragments, so derived metadata like
|
|
360
|
+
* `bodyLength` still reaches a log. Note that a name containing a redacted
|
|
361
|
+
* fragment is still caught: `promptTokens` is redacted because it contains
|
|
362
|
+
* "token". Over-redacting a counter is the right side to err on.
|
|
363
|
+
*/
|
|
364
|
+
export declare const REDACTED_FIELDS_EXACT: readonly ["body", "data", "payload", "prompt"];
|
|
365
|
+
/**
|
|
366
|
+
* Fields that contain "key" but are deliberately non-secret, and which
|
|
367
|
+
* observability depends on.
|
|
368
|
+
*
|
|
369
|
+
* `Callback-Key-Id` names *which* signing key was used and is the one thing
|
|
370
|
+
* needed to diagnose a rotation failure; `partitionKey` is a customer-facing
|
|
371
|
+
* forecast dimension; `idempotencyKey` is how a replay is traced. Redacting
|
|
372
|
+
* these would satisfy the letter of "hide secrets" while breaking the
|
|
373
|
+
* observability the architecture requires.
|
|
374
|
+
*/
|
|
375
|
+
export declare const NEVER_REDACTED: readonly ["callbackkeyid", "idempotencykey", "keyid", "partitionkey"];
|
|
376
|
+
/**
|
|
377
|
+
* Redacts by field name, recursively, for structured logging.
|
|
378
|
+
*
|
|
379
|
+
* Customer callback payloads are content, not metadata: `data` and `payload`
|
|
380
|
+
* are redacted wholesale rather than sampled, because a scheduling payload is
|
|
381
|
+
* exactly the kind of thing that carries a customer's own secrets.
|
|
382
|
+
*/
|
|
383
|
+
export declare function redact(value: unknown): unknown;
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { environmentKindSchema, scopeSchema } from "./authority.js";
|
|
3
|
+
import { apiCredentialIdSchema, projectIdSchema } from "./identifiers.js";
|
|
4
|
+
import { pageSchema } from "./protocol.js";
|
|
5
|
+
import { durationSecondsSchema, timestampSchema } from "./time.js";
|
|
6
|
+
/**
|
|
7
|
+
* Secret handling and redaction.
|
|
8
|
+
*
|
|
9
|
+
* A secret is shown exactly once, at creation. After that only its non-secret
|
|
10
|
+
* prefix and metadata are readable — by the API, by MCP tools, by the
|
|
11
|
+
* dashboard, and by support. There is no "reveal" endpoint, because a store
|
|
12
|
+
* that can reveal a secret is a store that can leak one.
|
|
13
|
+
*/
|
|
14
|
+
/** `pt_` + environment + `_` + public prefix. The secret half is never stored. */
|
|
15
|
+
export const CREDENTIAL_PREFIX_PATTERN = /^pt_(?:live|test)_[0-9a-z]{8}$/u;
|
|
16
|
+
/** How many characters of a credential are the non-secret display prefix. */
|
|
17
|
+
export const CREDENTIAL_PREFIX_LENGTH = 16;
|
|
18
|
+
/** How many characters of high-entropy secret follow the display prefix. */
|
|
19
|
+
export const CREDENTIAL_SECRET_LENGTH = 32;
|
|
20
|
+
/**
|
|
21
|
+
* A whole credential: the display prefix, followed immediately by the secret
|
|
22
|
+
* half. `pt_test_a1b2c3d4` is the prefix of `pt_test_a1b2c3d4<32 more>`, which
|
|
23
|
+
* is what lets a presented credential be looked up by prefix before anything is
|
|
24
|
+
* compared — a store that had to scan every row to find a candidate would leak
|
|
25
|
+
* timing, and a separator would be one more thing for a client to get wrong.
|
|
26
|
+
*
|
|
27
|
+
* **This pattern is enforced where credentials are issued and verified, and
|
|
28
|
+
* deliberately not by `credentialCreatedSchema` below.** A response schema is
|
|
29
|
+
* projected into a committed example, and an example matching this pattern is
|
|
30
|
+
* indistinguishable from a real credential to `npm run check:secrets`, which
|
|
31
|
+
* would refuse it — correctly. So the wire schema states the length floor, the
|
|
32
|
+
* issuer states the format, and the published example is visibly a placeholder.
|
|
33
|
+
*/
|
|
34
|
+
export const CREDENTIAL_SECRET_PATTERN = /^pt_(?:live|test)_[0-9a-z]{40}$/u;
|
|
35
|
+
/**
|
|
36
|
+
* A credential as any read shape shows it: what it can do, where it can do it,
|
|
37
|
+
* and enough to recognise which one it is. Never the secret.
|
|
38
|
+
*/
|
|
39
|
+
export const credentialMetadataSchema = z.object({
|
|
40
|
+
createdAt: timestampSchema,
|
|
41
|
+
/** The non-secret leading segment, safe to display and log. */
|
|
42
|
+
displayPrefix: z.string().regex(CREDENTIAL_PREFIX_PATTERN),
|
|
43
|
+
/** Which environment of that project. Part of the credential's authority. */
|
|
44
|
+
environmentKind: environmentKindSchema,
|
|
45
|
+
/**
|
|
46
|
+
* When this credential stops verifying, present only after it is rotated.
|
|
47
|
+
*
|
|
48
|
+
* Distinct from `revokedAt` because the two say different things to whoever
|
|
49
|
+
* is debugging: an expiry is self-inflicted and scheduled — you rotated, and
|
|
50
|
+
* this is the deadline you were given — where a revocation is somebody
|
|
51
|
+
* deciding this credential should stop working now. Collapsing them would
|
|
52
|
+
* make a missed rotation window look like a security event.
|
|
53
|
+
*/
|
|
54
|
+
expiresAt: timestampSchema.optional(),
|
|
55
|
+
id: apiCredentialIdSchema,
|
|
56
|
+
/**
|
|
57
|
+
* The member who issued it, when one is recorded.
|
|
58
|
+
*
|
|
59
|
+
* **Published because acting on somebody else's credential needs a wider
|
|
60
|
+
* scope, and a surface that could not say whose a credential is would have to
|
|
61
|
+
* discover that by being refused.** The value is the identifier the identity
|
|
62
|
+
* provider uses for that person; it names nobody outside the organization
|
|
63
|
+
* that already shares the credential's project.
|
|
64
|
+
*
|
|
65
|
+
* Absent on a credential issued before the issuer was recorded, and on one
|
|
66
|
+
* issued by anything that is not a signed-in member. An absent issuer is
|
|
67
|
+
* *nobody's* — it matches no member, so only a caller holding
|
|
68
|
+
* `credentials:administer` may rotate or revoke it.
|
|
69
|
+
*/
|
|
70
|
+
issuedBy: z.string().min(1).optional(),
|
|
71
|
+
lastUsedAt: timestampSchema.optional(),
|
|
72
|
+
/** The project this credential is bound to for its whole life. */
|
|
73
|
+
project: projectIdSchema,
|
|
74
|
+
revokedAt: timestampSchema.optional(),
|
|
75
|
+
/**
|
|
76
|
+
* The credential that replaced this one, present only on a rotated
|
|
77
|
+
* credential. Read shapes carry it so a holder of the old credential can tell
|
|
78
|
+
* that a replacement exists without being able to use it — the id is not a
|
|
79
|
+
* secret, and it is what makes "why did this stop working" answerable.
|
|
80
|
+
*/
|
|
81
|
+
supersededBy: apiCredentialIdSchema.optional(),
|
|
82
|
+
/**
|
|
83
|
+
* Exactly what it may do. Present on the read shape because a credential
|
|
84
|
+
* nobody can see the scopes of is a credential nobody can audit, and the
|
|
85
|
+
* scope list is not secret — holding it grants nothing.
|
|
86
|
+
*/
|
|
87
|
+
scopes: z.array(scopeSchema).min(1),
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* A page of credentials.
|
|
91
|
+
*
|
|
92
|
+
* Built from `pageSchema` rather than written out, so paging is defined once:
|
|
93
|
+
* the cursor, the bounded limit and the freshness discriminator on this
|
|
94
|
+
* collection are the same ones every later collection gets, and a client that
|
|
95
|
+
* can page one can page all of them.
|
|
96
|
+
*
|
|
97
|
+
* The item is `credentialMetadataSchema` — the read shape — so there is no
|
|
98
|
+
* shape in the contract in which a list could carry a secret. That is a
|
|
99
|
+
* property of the type rather than of the query behind it.
|
|
100
|
+
*/
|
|
101
|
+
export const credentialPageSchema = pageSchema(credentialMetadataSchema);
|
|
102
|
+
/**
|
|
103
|
+
* The one-time creation response. `secret` is present here and nowhere else in
|
|
104
|
+
* the contract; every read shape carries metadata only.
|
|
105
|
+
*/
|
|
106
|
+
export const credentialCreatedSchema = credentialMetadataSchema.extend({
|
|
107
|
+
secret: z.string().min(CREDENTIAL_PREFIX_LENGTH + CREDENTIAL_SECRET_LENGTH),
|
|
108
|
+
/** Always true, so a client cannot mistake this for a readable field. */
|
|
109
|
+
shownOnce: z.literal(true),
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* What a caller asks for when issuing a credential.
|
|
113
|
+
*
|
|
114
|
+
* `project` and `environmentKind` are caller-supplied and then *validated
|
|
115
|
+
* against the principal's organization* — they select within the caller's own
|
|
116
|
+
* tenancy rather than widening it, which is the same shape `resolveAuthority`
|
|
117
|
+
* already has. The organization is never here: that would be selecting a
|
|
118
|
+
* tenant, which `docs/contracts/v1-conventions.md` forbids outright.
|
|
119
|
+
*/
|
|
120
|
+
export const credentialRequestSchema = z.object({
|
|
121
|
+
environmentKind: environmentKindSchema,
|
|
122
|
+
project: projectIdSchema,
|
|
123
|
+
/**
|
|
124
|
+
* Requested scopes. The grant is the intersection of these, the credential
|
|
125
|
+
* policy, and the issuing member's current role — so asking for more than the
|
|
126
|
+
* role allows narrows the result rather than refusing it.
|
|
127
|
+
*/
|
|
128
|
+
scopes: z.array(scopeSchema).min(1),
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* The longest a rotated credential may keep working, and its default.
|
|
132
|
+
*
|
|
133
|
+
* The number is a judgement rather than a derivation: long enough that a deploy
|
|
134
|
+
* can roll a new secret through every consumer, short enough that a leaked old
|
|
135
|
+
* secret stops being useful within a day. It is stated here rather than left in
|
|
136
|
+
* a route so that changing it is a contract change somebody reviews.
|
|
137
|
+
*/
|
|
138
|
+
export const CREDENTIAL_ROTATION_OVERLAP_SECONDS = 86_400;
|
|
139
|
+
/**
|
|
140
|
+
* What a caller asks for when rotating.
|
|
141
|
+
*
|
|
142
|
+
* `overlapSeconds` may only *shorten* the window — the default is also the
|
|
143
|
+
* maximum. A caller who knows their consumers have already picked up the new
|
|
144
|
+
* secret can retire the old one immediately; nobody can ask for a longer tail
|
|
145
|
+
* than the contract allows, because the tail is the part that is dangerous.
|
|
146
|
+
*
|
|
147
|
+
* Zero is permitted and means the old credential stops verifying at once, which
|
|
148
|
+
* is deliberately not the same as revoking it: the audit trail records a
|
|
149
|
+
* rotation, and the retired credential names its replacement.
|
|
150
|
+
*/
|
|
151
|
+
export const credentialRotationRequestSchema = z.object({
|
|
152
|
+
overlapSeconds: durationSecondsSchema
|
|
153
|
+
.max(CREDENTIAL_ROTATION_OVERLAP_SECONDS, "must not exceed the maximum rotation overlap")
|
|
154
|
+
.optional(),
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* The rotation response: the replacement, and what became of the original.
|
|
158
|
+
*
|
|
159
|
+
* Both halves are here because a caller that only learned the new secret would
|
|
160
|
+
* have no way to know when the old one stops working, and the deadline is the
|
|
161
|
+
* whole point of an overlap. `retired` is metadata only — rotation never
|
|
162
|
+
* reveals a secret it did not just mint.
|
|
163
|
+
*/
|
|
164
|
+
export const credentialRotatedSchema = z.object({
|
|
165
|
+
credential: credentialCreatedSchema,
|
|
166
|
+
retired: credentialMetadataSchema,
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* The revocation response.
|
|
170
|
+
*
|
|
171
|
+
* Metadata rather than an empty body, because the contract has no
|
|
172
|
+
* representation for one — `successStatusFor` yields 200 or 201 and every
|
|
173
|
+
* operation must declare a response schema. That constraint turns out to be the
|
|
174
|
+
* better answer anyway: returning the revoked credential lets a caller confirm
|
|
175
|
+
* *which* credential stopped working and when, which an empty 204 would make
|
|
176
|
+
* them ask for separately.
|
|
177
|
+
*/
|
|
178
|
+
export const credentialRevokedSchema = z.object({
|
|
179
|
+
credential: credentialMetadataSchema,
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* Field-name fragments whose values must never reach a log, metric, or support
|
|
183
|
+
* index.
|
|
184
|
+
*
|
|
185
|
+
* Matched as substrings against a normalized key rather than by exact name.
|
|
186
|
+
* Exact matching caught `token` and missed `refreshToken`, `access_token`,
|
|
187
|
+
* `apiKey`, `client_secret` and `password` — which is most of what
|
|
188
|
+
* architecture section 10 actually requires be kept out of logs. Naming
|
|
189
|
+
* conventions vary across the codebase and across the libraries we log from,
|
|
190
|
+
* so the rule has to be insensitive to them.
|
|
191
|
+
*/
|
|
192
|
+
export const REDACTED_FIELD_FRAGMENTS = [
|
|
193
|
+
"authorization",
|
|
194
|
+
"cookie",
|
|
195
|
+
"credential",
|
|
196
|
+
"key",
|
|
197
|
+
"password",
|
|
198
|
+
"secret",
|
|
199
|
+
"signature",
|
|
200
|
+
"token",
|
|
201
|
+
];
|
|
202
|
+
/**
|
|
203
|
+
* Fields redacted wholesale because they are content, not metadata.
|
|
204
|
+
*
|
|
205
|
+
* Architecture section 10 names callback bodies and prompts alongside
|
|
206
|
+
* payloads. Matched exactly rather than as fragments, so derived metadata like
|
|
207
|
+
* `bodyLength` still reaches a log. Note that a name containing a redacted
|
|
208
|
+
* fragment is still caught: `promptTokens` is redacted because it contains
|
|
209
|
+
* "token". Over-redacting a counter is the right side to err on.
|
|
210
|
+
*/
|
|
211
|
+
export const REDACTED_FIELDS_EXACT = [
|
|
212
|
+
"body",
|
|
213
|
+
"data",
|
|
214
|
+
"payload",
|
|
215
|
+
"prompt",
|
|
216
|
+
];
|
|
217
|
+
/**
|
|
218
|
+
* Fields that contain "key" but are deliberately non-secret, and which
|
|
219
|
+
* observability depends on.
|
|
220
|
+
*
|
|
221
|
+
* `Callback-Key-Id` names *which* signing key was used and is the one thing
|
|
222
|
+
* needed to diagnose a rotation failure; `partitionKey` is a customer-facing
|
|
223
|
+
* forecast dimension; `idempotencyKey` is how a replay is traced. Redacting
|
|
224
|
+
* these would satisfy the letter of "hide secrets" while breaking the
|
|
225
|
+
* observability the architecture requires.
|
|
226
|
+
*/
|
|
227
|
+
export const NEVER_REDACTED = [
|
|
228
|
+
// The header spelling matters as much as the bare one: a log line carries
|
|
229
|
+
// `Callback-Key-Id` under its real name, which normalizes to
|
|
230
|
+
// `callbackkeyid` and would otherwise miss an exact-match allowlist and be
|
|
231
|
+
// caught by the `key` fragment — leaving rotation diagnosis blind, which is
|
|
232
|
+
// the exact failure this list exists to prevent.
|
|
233
|
+
"callbackkeyid",
|
|
234
|
+
"idempotencykey",
|
|
235
|
+
"keyid",
|
|
236
|
+
"partitionkey",
|
|
237
|
+
];
|
|
238
|
+
const REDACTION = "[redacted]";
|
|
239
|
+
/** Lowercased with separators removed, so `access_token` and `accessToken` agree. */
|
|
240
|
+
function normalizeKey(key) {
|
|
241
|
+
return key.toLowerCase().replace(/[_-]/gu, "");
|
|
242
|
+
}
|
|
243
|
+
function mustRedact(key) {
|
|
244
|
+
const normalized = normalizeKey(key);
|
|
245
|
+
// The allowlist wins, so broad fragment matching cannot swallow a field
|
|
246
|
+
// whose whole purpose is to be readable in a log. Matched whole, not as a
|
|
247
|
+
// suffix: `apiKeyId` ends with `keyid` and must still be redacted.
|
|
248
|
+
if (NEVER_REDACTED.some((allowed) => normalized === allowed)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return (REDACTED_FIELDS_EXACT.some((exact) => normalized === exact) ||
|
|
252
|
+
REDACTED_FIELD_FRAGMENTS.some((fragment) => normalized.includes(normalizeKey(fragment))));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Redacts by field name, recursively, for structured logging.
|
|
256
|
+
*
|
|
257
|
+
* Customer callback payloads are content, not metadata: `data` and `payload`
|
|
258
|
+
* are redacted wholesale rather than sampled, because a scheduling payload is
|
|
259
|
+
* exactly the kind of thing that carries a customer's own secrets.
|
|
260
|
+
*/
|
|
261
|
+
export function redact(value) {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
return value.map(redact);
|
|
264
|
+
}
|
|
265
|
+
if (typeof value !== "object" || value === null) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
|
|
269
|
+
key,
|
|
270
|
+
mustRedact(key) ? REDACTION : redact(nested),
|
|
271
|
+
]));
|
|
272
|
+
}
|