@abloatai/transaction 0.37.1 → 0.38.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.
Files changed (148) hide show
  1. package/dist/ai-sdk/index.d.ts +3 -1
  2. package/dist/ai-sdk/index.d.ts.map +1 -1
  3. package/dist/ai-sdk/index.js +2 -1
  4. package/dist/ai-sdk/index.js.map +1 -1
  5. package/dist/ai-sdk/modelTools.d.ts +67 -0
  6. package/dist/ai-sdk/modelTools.d.ts.map +1 -0
  7. package/dist/ai-sdk/modelTools.js +89 -0
  8. package/dist/ai-sdk/modelTools.js.map +1 -0
  9. package/dist/ai-sdk/toolOptions.d.ts +8 -0
  10. package/dist/ai-sdk/toolOptions.d.ts.map +1 -0
  11. package/dist/ai-sdk/toolOptions.js +2 -0
  12. package/dist/ai-sdk/toolOptions.js.map +1 -0
  13. package/dist/ai-sdk/{coordinatedTool.d.ts → updateTool.d.ts} +21 -28
  14. package/dist/ai-sdk/updateTool.d.ts.map +1 -0
  15. package/dist/ai-sdk/{coordinatedTool.js → updateTool.js} +39 -50
  16. package/dist/ai-sdk/updateTool.js.map +1 -0
  17. package/dist/auth/capability.d.ts +6 -0
  18. package/dist/auth/capability.d.ts.map +1 -1
  19. package/dist/auth/capability.js +4 -0
  20. package/dist/auth/capability.js.map +1 -1
  21. package/dist/auth/capabilityLifecycle.d.ts +3 -0
  22. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  23. package/dist/auth/identity.d.ts +6 -4
  24. package/dist/auth/identity.d.ts.map +1 -1
  25. package/dist/auth/identity.js +7 -11
  26. package/dist/auth/identity.js.map +1 -1
  27. package/dist/auth/schemas.d.ts +5 -5
  28. package/dist/auth/schemas.d.ts.map +1 -1
  29. package/dist/auth/schemas.js +5 -2
  30. package/dist/auth/schemas.js.map +1 -1
  31. package/dist/auth/sessionMint.d.ts.map +1 -1
  32. package/dist/auth/sessionMint.js +3 -0
  33. package/dist/auth/sessionMint.js.map +1 -1
  34. package/dist/branches.d.ts +251 -0
  35. package/dist/branches.d.ts.map +1 -0
  36. package/dist/branches.js +108 -0
  37. package/dist/branches.js.map +1 -0
  38. package/dist/coordination/awaitClaimGrant.d.ts +2 -0
  39. package/dist/coordination/awaitClaimGrant.d.ts.map +1 -1
  40. package/dist/coordination/awaitClaimGrant.js +15 -2
  41. package/dist/coordination/awaitClaimGrant.js.map +1 -1
  42. package/dist/coordination/schema.d.ts +2 -0
  43. package/dist/coordination/schema.d.ts.map +1 -1
  44. package/dist/coordination/schema.js +2 -0
  45. package/dist/coordination/schema.js.map +1 -1
  46. package/dist/docs/catalog.d.ts +5 -5
  47. package/dist/docs/catalog.d.ts.map +1 -1
  48. package/dist/docs/catalog.js +12 -12
  49. package/dist/docs/catalog.js.map +1 -1
  50. package/dist/footprint.d.ts +3 -5
  51. package/dist/footprint.d.ts.map +1 -1
  52. package/dist/footprint.js +0 -0
  53. package/dist/footprint.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/keys/index.d.ts +15 -7
  59. package/dist/keys/index.d.ts.map +1 -1
  60. package/dist/keys/index.js +45 -14
  61. package/dist/keys/index.js.map +1 -1
  62. package/dist/schema/openapi.d.ts +1 -1
  63. package/dist/schema/openapi.d.ts.map +1 -1
  64. package/dist/schema/openapi.js +308 -29
  65. package/dist/schema/openapi.js.map +1 -1
  66. package/dist/server/commit.d.ts +2 -15
  67. package/dist/server/commit.d.ts.map +1 -1
  68. package/dist/source/adapter.d.ts +7 -2
  69. package/dist/source/adapter.d.ts.map +1 -1
  70. package/dist/source/adapter.js +4 -2
  71. package/dist/source/adapter.js.map +1 -1
  72. package/dist/source/adapterFactory.d.ts +10 -0
  73. package/dist/source/adapterFactory.d.ts.map +1 -0
  74. package/dist/source/adapterFactory.js +33 -0
  75. package/dist/source/adapterFactory.js.map +1 -0
  76. package/dist/source/adapterProfile.d.ts +55 -0
  77. package/dist/source/adapterProfile.d.ts.map +1 -0
  78. package/dist/source/adapterProfile.js +54 -0
  79. package/dist/source/adapterProfile.js.map +1 -0
  80. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  81. package/dist/source/adapters/drizzle.js +5 -2
  82. package/dist/source/adapters/drizzle.js.map +1 -1
  83. package/dist/source/adapters/kysely.d.ts.map +1 -1
  84. package/dist/source/adapters/kysely.js +7 -4
  85. package/dist/source/adapters/kysely.js.map +1 -1
  86. package/dist/source/adapters/memory.d.ts.map +1 -1
  87. package/dist/source/adapters/memory.js +5 -2
  88. package/dist/source/adapters/memory.js.map +1 -1
  89. package/dist/source/adapters/prisma.d.ts.map +1 -1
  90. package/dist/source/adapters/prisma.js +5 -2
  91. package/dist/source/adapters/prisma.js.map +1 -1
  92. package/dist/source/conformance.d.ts.map +1 -1
  93. package/dist/source/conformance.js +18 -0
  94. package/dist/source/conformance.js.map +1 -1
  95. package/dist/source/index.d.ts +2 -0
  96. package/dist/source/index.d.ts.map +1 -1
  97. package/dist/source/index.js +2 -0
  98. package/dist/source/index.js.map +1 -1
  99. package/dist/source/types.d.ts +3 -14
  100. package/dist/source/types.d.ts.map +1 -1
  101. package/dist/source/types.js.map +1 -1
  102. package/dist/syncLog/contract.d.ts +1 -1
  103. package/dist/syncLog/contract.d.ts.map +1 -1
  104. package/dist/syncLog/contract.js +1 -2
  105. package/dist/syncLog/contract.js.map +1 -1
  106. package/dist/wire/accountResponses.d.ts +5 -9
  107. package/dist/wire/accountResponses.d.ts.map +1 -1
  108. package/dist/wire/accountResponses.js +5 -6
  109. package/dist/wire/accountResponses.js.map +1 -1
  110. package/dist/wire/delta.d.ts +11 -0
  111. package/dist/wire/delta.d.ts.map +1 -1
  112. package/dist/wire/delta.js +11 -0
  113. package/dist/wire/delta.js.map +1 -1
  114. package/dist/wire/inboundFrames.d.ts +2 -0
  115. package/dist/wire/inboundFrames.d.ts.map +1 -1
  116. package/package.json +8 -2
  117. package/src/ai-sdk/index.ts +19 -6
  118. package/src/ai-sdk/modelTools.ts +165 -0
  119. package/src/ai-sdk/toolOptions.ts +18 -0
  120. package/src/ai-sdk/{coordinatedTool.ts → updateTool.ts} +59 -69
  121. package/src/auth/capability.ts +4 -0
  122. package/src/auth/identity.ts +16 -16
  123. package/src/auth/schemas.ts +5 -2
  124. package/src/auth/sessionMint.ts +3 -0
  125. package/src/branches.ts +134 -0
  126. package/src/coordination/awaitClaimGrant.ts +18 -2
  127. package/src/coordination/schema.ts +2 -0
  128. package/src/docs/catalog.ts +24 -12
  129. package/src/footprint.ts +0 -0
  130. package/src/index.ts +1 -0
  131. package/src/keys/index.ts +56 -17
  132. package/src/schema/openapi.ts +366 -38
  133. package/src/server/commit.ts +2 -15
  134. package/src/source/adapter.ts +7 -2
  135. package/src/source/adapterFactory.ts +60 -0
  136. package/src/source/adapterProfile.ts +75 -0
  137. package/src/source/adapters/drizzle.ts +5 -2
  138. package/src/source/adapters/kysely.ts +10 -4
  139. package/src/source/adapters/memory.ts +5 -2
  140. package/src/source/adapters/prisma.ts +5 -2
  141. package/src/source/conformance.ts +18 -0
  142. package/src/source/index.ts +14 -0
  143. package/src/source/types.ts +3 -14
  144. package/src/syncLog/contract.ts +1 -2
  145. package/src/wire/accountResponses.ts +5 -6
  146. package/src/wire/delta.ts +11 -0
  147. package/dist/ai-sdk/coordinatedTool.d.ts.map +0 -1
  148. package/dist/ai-sdk/coordinatedTool.js.map +0 -1
@@ -55,6 +55,8 @@ export interface ClaimGrantInfo {
55
55
  * `undefined` when the server does not fence (no minter wired).
56
56
  */
57
57
  readonly fenceToken?: number;
58
+ /** Authoritative branch watermark captured when the lease was granted. */
59
+ readonly readAt?: number;
58
60
  }
59
61
 
60
62
  /** Read the server-stamped fencing token off a grant frame, if present. */
@@ -62,6 +64,10 @@ function readFenceToken(p: Record<string, unknown>): number | undefined {
62
64
  return typeof p.fenceToken === 'number' ? p.fenceToken : undefined;
63
65
  }
64
66
 
67
+ function readWatermark(p: Record<string, unknown>): number | undefined {
68
+ return typeof p.readAt === 'number' ? p.readAt : undefined;
69
+ }
70
+
65
71
  export function awaitClaimGrant(
66
72
  transport: GrantTransport,
67
73
  claimId: string,
@@ -102,8 +108,13 @@ export function awaitClaimGrant(
102
108
  if (p?.claimId === claimId) {
103
109
  logger.debug(`claim: acquired ${claimId} (target was free)`);
104
110
  const fenceToken = readFenceToken(p);
111
+ const readAt = readWatermark(p);
105
112
  settle(() => {
106
- resolve({ waited: false, ...(fenceToken !== undefined ? { fenceToken } : {}) });
113
+ resolve({
114
+ waited: false,
115
+ ...(fenceToken !== undefined ? { fenceToken } : {}),
116
+ ...(readAt !== undefined ? { readAt } : {}),
117
+ });
107
118
  });
108
119
  }
109
120
  }),
@@ -115,8 +126,13 @@ export function awaitClaimGrant(
115
126
  // turn now" moment after waiting behind a holder.
116
127
  logger.info(`claim: granted ${claimId} — your turn (waited in queue)`);
117
128
  const fenceToken = readFenceToken(p);
129
+ const readAt = readWatermark(p);
118
130
  settle(() => {
119
- resolve({ waited: true, ...(fenceToken !== undefined ? { fenceToken } : {}) });
131
+ resolve({
132
+ waited: true,
133
+ ...(fenceToken !== undefined ? { fenceToken } : {}),
134
+ ...(readAt !== undefined ? { readAt } : {}),
135
+ });
120
136
  });
121
137
  }
122
138
  }),
@@ -626,6 +626,7 @@ export type ClaimLost = z.infer<typeof claimLostSchema>;
626
626
  export const claimAcquiredSchema = z.object({
627
627
  claimId: z.string(),
628
628
  fenceToken: z.number().optional(),
629
+ readAt: z.number().int().nonnegative().optional(),
629
630
  target: targetRefSchema,
630
631
  });
631
632
  export type ClaimAcquired = z.infer<typeof claimAcquiredSchema>;
@@ -640,6 +641,7 @@ export type ClaimAcquired = z.infer<typeof claimAcquiredSchema>;
640
641
  export const claimGrantedSchema = z.object({
641
642
  claimId: z.string(),
642
643
  fenceToken: z.number().optional(),
644
+ readAt: z.number().int().nonnegative().optional(),
643
645
  target: targetRefSchema,
644
646
  });
645
647
  export type ClaimGranted = z.infer<typeof claimGrantedSchema>;
@@ -14,11 +14,11 @@
14
14
  * same line — so a page states its own promise exactly once, in the source that
15
15
  * ships in the npm tarball and still reads correctly as plain markdown.
16
16
  *
17
- * The published set mirrors the tarball's `files` globs (`docs/*.md`,
18
- * `docs/examples/*.md`, plus the three root files). Anything else under `docs/`
19
- * — `docs/internal/**` above all — is neither shipped nor catalogued, and that
20
- * exclusion is the only thing keeping internal notes off the public routes now
21
- * that they read from here.
17
+ * The published set is an explicit allowlist: `docs/*.md`,
18
+ * `docs/examples/*.md`, `docs/integrations/*.md`, plus the three root files.
19
+ * Anything else under `docs/` — `docs/internal/**` above all — is not
20
+ * catalogued, and that exclusion is the only thing keeping internal notes off
21
+ * the public routes now that they read from here.
22
22
  */
23
23
 
24
24
  import { readdir, readFile } from 'node:fs/promises';
@@ -115,7 +115,16 @@ export async function readDocsCatalog(packageRoot: string): Promise<DocEntry[]>
115
115
  const entries: DocEntry[] = [];
116
116
 
117
117
  entries.push(...(await readDocsDirectory(docsDir, 'guide')));
118
- entries.push(...(await readDocsDirectory(join(docsDir, 'examples'), 'example')));
118
+ entries.push(
119
+ ...(await readDocsDirectory(join(docsDir, 'examples'), 'example', 'examples')),
120
+ );
121
+ entries.push(
122
+ ...(await readDocsDirectory(
123
+ join(docsDir, 'integrations'),
124
+ 'guide',
125
+ 'integrations',
126
+ )),
127
+ );
119
128
 
120
129
  for (const file of PACKAGE_FILES) {
121
130
  const path = join(packageRoot, file.filename);
@@ -135,12 +144,15 @@ export async function readDocsCatalog(packageRoot: string): Promise<DocEntry[]>
135
144
  }
136
145
 
137
146
  /**
138
- * Read one flat directory of markdown. Deliberately non-recursive: the shipped
139
- * set is `docs/*.md` plus `docs/examples/*.md`, so descending would catalogue
140
- * `docs/internal/**` — notes that are not in the tarball and must not reach a
141
- * public route.
147
+ * Read one explicitly allowed flat directory of markdown. Deliberately
148
+ * non-recursive: descending from `docs/` would catalogue `docs/internal/**`,
149
+ * notes that must not reach a public route.
142
150
  */
143
- async function readDocsDirectory(dir: string, kind: DocKind): Promise<DocEntry[]> {
151
+ async function readDocsDirectory(
152
+ dir: string,
153
+ kind: DocKind,
154
+ slugPrefix?: string,
155
+ ): Promise<DocEntry[]> {
144
156
  let names: string[];
145
157
  try {
146
158
  const found = await readdir(dir, { withFileTypes: true });
@@ -157,7 +169,7 @@ async function readDocsDirectory(dir: string, kind: DocKind): Promise<DocEntry[]
157
169
  const stem = basename(name, '.md');
158
170
  const { title, description } = parseDocHeader(body);
159
171
  entries.push({
160
- slug: kind === 'example' ? `examples/${stem}` : stem,
172
+ slug: slugPrefix ? `${slugPrefix}/${stem}` : stem,
161
173
  title: title ?? stem,
162
174
  description,
163
175
  kind,
package/src/footprint.ts CHANGED
Binary file
package/src/index.ts CHANGED
@@ -50,3 +50,4 @@ export * from './errors.js';
50
50
  // Pre-seam leaf surfaces, kept for compatibility with the first extraction.
51
51
  export * from './errorCodes.js';
52
52
  export * from './environment.js';
53
+ export * from './branches.js';
package/src/keys/index.ts CHANGED
@@ -6,9 +6,10 @@
6
6
  * the `@abloatai/transaction/keys` subpath and kept off the main browser-facing entry
7
7
  * so a browser bundle never pulls in `node:crypto`.
8
8
  *
9
- * A key looks like `<sk|rk|ek|pk>_<live|test>_<30 base62 chars><6-char base62
10
- * CRC32 checksum>`. The middle segment is the stable environment prefix, mapped
11
- * on parse to `production` or `sandbox`. The recognizable prefix lets secret
9
+ * A data-plane key looks like
10
+ * `<sk|rk|ek|pk>_<live|test>_<30 base62 chars><6-char base62 CRC32 checksum>`.
11
+ * A control-plane management key is `mk_<body><checksum>` and deliberately has
12
+ * no live/test segment: it cannot access application data. The recognizable prefix lets secret
12
13
  * scanners spot a leaked key, and the trailing checksum lets the format reject a
13
14
  * mistyped or forged key locally, without a database round-trip. Older keys
14
15
  * (roughly a 43-character base64url body with no checksum) still validate by hash
@@ -28,7 +29,10 @@ import {
28
29
 
29
30
  // ── Vocabulary ──────────────────────────────────────────────────────────
30
31
 
31
- // The four key kinds:
32
+ // The five credential kinds:
33
+ // management (mk_) — project control-plane authority. It can manage projects
34
+ // and branches and mint leaf branch credentials, but has
35
+ // no data-plane read/write authority and no livemode.
32
36
  // secret (sk_) — backend and server-to-server use, including agents. Full
33
37
  // authority; never expose one in a browser.
34
38
  // restricted (rk_) — a scoped server key, such as an agent session token or a
@@ -41,7 +45,13 @@ import {
41
45
  // exchanged, never expires, nothing to refresh. It grants
42
46
  // read access to the organization's data and cannot write or
43
47
  // reach any control-plane operation.
44
- export const API_KEY_KINDS = ['secret', 'restricted', 'ephemeral', 'publishable'] as const;
48
+ export const API_KEY_KINDS = [
49
+ 'management',
50
+ 'secret',
51
+ 'restricted',
52
+ 'ephemeral',
53
+ 'publishable',
54
+ ] as const;
45
55
  export type ApiKeyKind = (typeof API_KEY_KINDS)[number];
46
56
 
47
57
  // A key's environment is the CREDENTIAL axis, not the plane axis: the format
@@ -51,12 +61,14 @@ export const API_KEY_ENVS = KEY_ENVIRONMENTS;
51
61
  export type ApiKeyEnv = KeyEnvironment;
52
62
 
53
63
  const PREFIX_BY_KIND: Record<ApiKeyKind, string> = {
64
+ management: 'mk',
54
65
  secret: 'sk',
55
66
  restricted: 'rk',
56
67
  ephemeral: 'ek',
57
68
  publishable: 'pk',
58
69
  };
59
70
  const KIND_BY_PREFIX: Record<string, ApiKeyKind> = {
71
+ mk: 'management',
60
72
  sk: 'secret',
61
73
  rk: 'restricted',
62
74
  ek: 'ephemeral',
@@ -72,7 +84,9 @@ const CHECKSUM_LEN = 6;
72
84
  const CHECKSUMMED_BODY_LEN = KEY_BODY_LEN + CHECKSUM_LEN;
73
85
 
74
86
  /** `<sk|rk|ek|pk>_<live|test>_<body>`; the body charset covers base62 as well as the legacy base64url form. */
75
- const KEY_RE = /^(sk|rk|ek|pk)_(live|test)_([0-9A-Za-z\-_]+)$/;
87
+ const DATA_KEY_RE = /^(sk|rk|ek|pk)_(live|test)_([0-9A-Za-z\-_]+)$/;
88
+ /** `mk_<body>`; management credentials have no data livemode segment. */
89
+ const MANAGEMENT_KEY_RE = /^(mk)_([0-9A-Za-z\-_]+)$/;
76
90
  const BASE62_RE = /^[0-9A-Za-z]+$/;
77
91
 
78
92
  // ── Checksum (standard CRC-32, GitHub-compatible) ───────────────────────
@@ -128,8 +142,9 @@ export interface ParsedApiKey {
128
142
  /** The original plaintext. */
129
143
  raw: string;
130
144
  kind: ApiKeyKind;
131
- env: ApiKeyEnv;
132
- /** The chars after `<prefix>_<env>_` (body + checksum for new keys). */
145
+ /** Null for `mk_`, which has no data-plane livemode. */
146
+ env: ApiKeyEnv | null;
147
+ /** The random body + checksum after the recognizable prefix. */
133
148
  body: string;
134
149
  /** True when this is the new checksummed format (36-char base62 body). */
135
150
  checksummed: boolean;
@@ -146,17 +161,20 @@ function bodyIsChecksummed(body: string): boolean {
146
161
  * `checksummed: false` and is left for the server to validate by hash.
147
162
  */
148
163
  export const apiKeySchema = z.string().transform((raw, ctx): ParsedApiKey => {
149
- const m = KEY_RE.exec(raw);
150
- if (!m) {
164
+ const dataMatch = DATA_KEY_RE.exec(raw);
165
+ const managementMatch = MANAGEMENT_KEY_RE.exec(raw);
166
+ if (!dataMatch && !managementMatch) {
151
167
  ctx.addIssue({ code: 'custom', message: 'not a valid Ablo API key format' });
152
168
  return z.NEVER;
153
169
  }
154
- const [, prefix, env, body] = m;
170
+ const prefix = dataMatch?.[1] ?? managementMatch?.[1];
171
+ const env = dataMatch?.[2];
172
+ const body = dataMatch?.[3] ?? managementMatch?.[2];
155
173
  const kind = prefix === undefined ? undefined : KIND_BY_PREFIX[prefix];
156
174
  // Unreachable on a KEY_RE match (all three groups are non-optional and the
157
175
  // prefix alternation is exactly the KIND_BY_PREFIX key set) — narrows the
158
176
  // regex-group lookups for the checks below.
159
- if (kind === undefined || env === undefined || body === undefined) {
177
+ if (kind === undefined || body === undefined) {
160
178
  ctx.addIssue({ code: 'custom', message: 'not a valid Ablo API key format' });
161
179
  return z.NEVER;
162
180
  }
@@ -168,7 +186,10 @@ export const apiKeySchema = z.string().transform((raw, ctx): ParsedApiKey => {
168
186
  return {
169
187
  raw,
170
188
  kind,
171
- env: environmentFromKeyPrefix(env as KeyPrefixEnvironment),
189
+ env:
190
+ kind === 'management'
191
+ ? null
192
+ : environmentFromKeyPrefix(env as KeyPrefixEnvironment),
172
193
  body,
173
194
  checksummed,
174
195
  };
@@ -199,13 +220,13 @@ export function environmentFromStoredKeyPrefix(prefix: string): KeyEnvironment |
199
220
 
200
221
  /** True when the key uses the new checksummed format (regardless of validity). */
201
222
  export function isChecksummedKey(raw: string): boolean {
202
- const body = KEY_RE.exec(raw)?.[3];
223
+ const body = DATA_KEY_RE.exec(raw)?.[3] ?? MANAGEMENT_KEY_RE.exec(raw)?.[2];
203
224
  return body !== undefined && bodyIsChecksummed(body);
204
225
  }
205
226
 
206
227
  /** Verify the embedded checksum. Meaningful only for checksummed-format keys. */
207
228
  export function keyChecksumMatches(raw: string): boolean {
208
- const body = KEY_RE.exec(raw)?.[3];
229
+ const body = DATA_KEY_RE.exec(raw)?.[3] ?? MANAGEMENT_KEY_RE.exec(raw)?.[2];
209
230
  if (body === undefined || !bodyIsChecksummed(body)) return false;
210
231
  return checksum6(raw.slice(0, -CHECKSUM_LEN)) === body.slice(KEY_BODY_LEN);
211
232
  }
@@ -217,15 +238,33 @@ export function keyChecksumMatches(raw: string): boolean {
217
238
  * once), its SHA-256 hash (persisted), and the 12-char display prefix.
218
239
  */
219
240
  export function generateApiKey(
220
- env: ApiKeyEnv = 'production',
241
+ env: ApiKeyEnv | null = 'production',
221
242
  kind: ApiKeyKind = 'secret',
222
243
  ): { plaintext: string; hash: string; prefix: string } {
223
244
  const body = randomBase62(KEY_BODY_LEN);
224
- const payload = `${PREFIX_BY_KIND[kind]}_${environmentToKeyPrefix(env)}_${body}`;
245
+ if (kind === 'management' && env !== null) {
246
+ throw new Error('management credentials do not have a live/test mode');
247
+ }
248
+ if (kind !== 'management' && env === null) {
249
+ throw new Error(`${kind} credentials require a live/test mode`);
250
+ }
251
+ const payload =
252
+ kind === 'management'
253
+ ? `${PREFIX_BY_KIND[kind]}_${body}`
254
+ : `${PREFIX_BY_KIND[kind]}_${environmentToKeyPrefix(env as ApiKeyEnv)}_${body}`;
225
255
  const plaintext = `${payload}${checksum6(payload)}`;
226
256
  return { plaintext, hash: hashApiKey(plaintext), prefix: plaintext.slice(0, 12) };
227
257
  }
228
258
 
259
+ /** Mint a project-scoped, control-plane-only credential. */
260
+ export function generateManagementKey(): {
261
+ plaintext: string;
262
+ hash: string;
263
+ prefix: string;
264
+ } {
265
+ return generateApiKey(null, 'management');
266
+ }
267
+
229
268
  /**
230
269
  * The stable SHA-256 hex digest of a plaintext key, computed both when a key is
231
270
  * minted and when one is looked up. A fast hash is the right choice here rather