@hadoku/worker-utils 1.6.20 → 1.7.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.
@@ -161,7 +161,33 @@ interface KeyRegistryRecord {
161
161
  * SESSIONS_KV binding.
162
162
  */
163
163
  repo?: string | null;
164
+ /**
165
+ * The PERSON holding this credential (added 2026-10-31, service grants):
166
+ * their own userId, while `userId` above stays the service identity's. A
167
+ * row with `heldBy` was minted for that person alone, so revoking them
168
+ * retires their rows and leaves every other holder of the identity working.
169
+ * Absent on a row from before grants, and on human sign-in keys.
170
+ */
171
+ heldBy?: string;
172
+ /**
173
+ * ms epoch after which the key authenticates nothing (added 2026-10-31).
174
+ * Every grant and fleet credential carries one (7 days from mint). Liveness
175
+ * is {@link isLiveKey}, never a bare `retiredAt` read; a sweep later stamps
176
+ * `retiredAt = expiresAt` so bookkeeping readers agree.
177
+ */
178
+ expiresAt?: number;
179
+ /** How a `heldBy` row was minted: a person under a grant, or fleet automation. */
180
+ via?: 'grant' | 'fleet';
164
181
  }
182
+ /**
183
+ * Does this row authenticate right now? Not retired, and not past `expiresAt`.
184
+ *
185
+ * THE liveness test. A path that decides whether a presented key is accepted
186
+ * must call this, never read `retiredAt` alone: an expired credential carries
187
+ * no `retiredAt` until the sweep reaches it, so a bare read would keep it
188
+ * working for up to a day past its expiry.
189
+ */
190
+ declare function isLiveKey(record: Pick<Partial<KeyRegistryRecord>, 'retiredAt' | 'expiresAt'> | null | undefined, now?: number): boolean;
165
191
  /** The minimum SESSIONS_KV surface this module uses. Read-only, by construction.
166
192
  *
167
193
  * `list` declares the PAGINATION fields, not just `keys`. Omitting them was not
@@ -273,6 +299,9 @@ interface RegistryRowMetadata {
273
299
  repo?: string | null;
274
300
  /** Present only as `true`, on a sign-up key (KeyRegistryRecord.internal). */
275
301
  internal?: true;
302
+ heldBy?: string;
303
+ expiresAt?: number;
304
+ via?: 'grant' | 'fleet';
276
305
  }
277
306
  /** Workers KV's ceiling on serialized list metadata, in bytes. */
278
307
  declare const REGISTRY_METADATA_MAX_BYTES = 1024;
@@ -474,4 +503,4 @@ declare function resolveGrantee(env: RegistryEnv, input: {
474
503
  key?: string;
475
504
  }): Promise<IdentityResult>;
476
505
 
477
- export { type EdgeIdentityBinding, type EdgeIdentityCall, type IdentityError, type IdentityResult, type KeyRegistryRecord, type KeyTier, MIN_SEARCH_QUERY_LENGTH, NO_REGISTRY, NO_USER_ID, REGISTRY_METADATA_MAX_BYTES, type RegistryEnv, type RegistryKvNamespace, type RegistryRowMetadata, type ResolvedIdentity, answersToName, isIdentityError, isRegistryNameTaken, liveRowsByName, readLiveRows, recordFromRegistryMetadata, registryNameMap, registryRowMetadata, resolveGrantee, resolveGranteeVia, resolveRegistryName, searchRegistryNames };
506
+ export { type EdgeIdentityBinding, type EdgeIdentityCall, type IdentityError, type IdentityResult, type KeyRegistryRecord, type KeyTier, MIN_SEARCH_QUERY_LENGTH, NO_REGISTRY, NO_USER_ID, REGISTRY_METADATA_MAX_BYTES, type RegistryEnv, type RegistryKvNamespace, type RegistryRowMetadata, type ResolvedIdentity, answersToName, isIdentityError, isLiveKey, isRegistryNameTaken, liveRowsByName, readLiveRows, recordFromRegistryMetadata, registryNameMap, registryRowMetadata, resolveGrantee, resolveGranteeVia, resolveRegistryName, searchRegistryNames };
package/dist/identity.js CHANGED
@@ -1,4 +1,8 @@
1
1
  // identity.ts
2
+ function isLiveKey(record, now = Date.now()) {
3
+ if (!record || record.retiredAt) return false;
4
+ return record.expiresAt === void 0 || record.expiresAt > now;
5
+ }
2
6
  function isIdentityError(result) {
3
7
  return "error" in result;
4
8
  }
@@ -36,6 +40,9 @@ function registryRowMetadata(record) {
36
40
  if (typeof record.retiredAt === "number") meta.retiredAt = record.retiredAt;
37
41
  if (typeof record.repo === "string" || record.repo === null) meta.repo = record.repo;
38
42
  if (record.internal === true) meta.internal = true;
43
+ if (typeof record.heldBy === "string") meta.heldBy = record.heldBy;
44
+ if (typeof record.expiresAt === "number") meta.expiresAt = record.expiresAt;
45
+ if (record.via === "grant" || record.via === "fleet") meta.via = record.via;
39
46
  const bytes = new TextEncoder().encode(JSON.stringify(meta)).length;
40
47
  return bytes <= REGISTRY_METADATA_MAX_BYTES ? meta : void 0;
41
48
  }
@@ -47,8 +54,10 @@ function recordFromRegistryMetadata(raw) {
47
54
  const out = { name: m.name };
48
55
  const str = (k) => m[k] === void 0 || typeof m[k] === "string";
49
56
  const num = (k) => m[k] === void 0 || typeof m[k] === "number";
50
- if (!str("tier") || !str("userId")) return null;
51
- if (!num("createdAt") || !num("lastSeenAt") || !num("retiredAt")) return null;
57
+ if (!str("tier") || !str("userId") || !str("heldBy")) return null;
58
+ if (!num("createdAt") || !num("lastSeenAt") || !num("retiredAt") || !num("expiresAt"))
59
+ return null;
60
+ if (m.via !== void 0 && m.via !== "grant" && m.via !== "fleet") return null;
52
61
  if (m.repo !== void 0 && m.repo !== null && typeof m.repo !== "string") return null;
53
62
  if (m.internal !== void 0 && m.internal !== true) return null;
54
63
  if (m.tier !== void 0) out.tier = m.tier;
@@ -58,6 +67,9 @@ function recordFromRegistryMetadata(raw) {
58
67
  if (m.retiredAt !== void 0) out.retiredAt = m.retiredAt;
59
68
  if (m.repo !== void 0) out.repo = m.repo;
60
69
  if (m.internal === true) out.internal = true;
70
+ if (m.heldBy !== void 0) out.heldBy = m.heldBy;
71
+ if (m.expiresAt !== void 0) out.expiresAt = m.expiresAt;
72
+ if (m.via !== void 0) out.via = m.via;
61
73
  return out;
62
74
  }
63
75
  function registryKv(env) {
@@ -93,7 +105,7 @@ async function scanRegistry(env) {
93
105
  async function readLiveRows(env) {
94
106
  const rows = [];
95
107
  for (const { record } of await scanRegistry(env)) {
96
- if (record.retiredAt || !record.userId) continue;
108
+ if (!isLiveKey(record) || !record.userId) continue;
97
109
  rows.push({ userId: record.userId, name: record.name ?? null, tier: record.tier });
98
110
  }
99
111
  return rows;
@@ -104,7 +116,7 @@ async function isRegistryNameTaken(env, name, exclude) {
104
116
  const excludeEntry = `${KEY_PREFIX}${exclude.rawKey}`;
105
117
  for (const { entry, record } of await scanRegistry(env)) {
106
118
  if (entry === excludeEntry) continue;
107
- if (record.retiredAt) continue;
119
+ if (!isLiveKey(record)) continue;
108
120
  if (exclude.userId && record.userId === exclude.userId) continue;
109
121
  if (record.name && foldName(record.name) === target) return true;
110
122
  }
@@ -119,7 +131,7 @@ async function resolveRegistryName(env, name) {
119
131
  const target = foldName(name);
120
132
  if (!target) return null;
121
133
  for (const { record } of await scanRegistry(env)) {
122
- if (record.retiredAt || !answersToName(record.tier)) continue;
134
+ if (!isLiveKey(record) || !answersToName(record.tier)) continue;
123
135
  if (record.name && foldName(record.name) === target) {
124
136
  return { name: record.name, userId: record.userId ?? null, tier: record.tier };
125
137
  }
@@ -226,6 +238,7 @@ export {
226
238
  REGISTRY_METADATA_MAX_BYTES,
227
239
  answersToName,
228
240
  isIdentityError,
241
+ isLiveKey,
229
242
  isRegistryNameTaken,
230
243
  liveRowsByName,
231
244
  readLiveRows,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadoku/worker-utils",
3
- "version": "1.6.20",
3
+ "version": "1.7.0",
4
4
  "license": "ISC",
5
5
  "description": "Shared utilities for Cloudflare Workers",
6
6
  "repository": {