@noy-db/hub 0.1.0-pre.8 → 0.1.0-pre.9

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 (77) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +2 -2
  3. package/dist/blobs/index.d.ts +2 -2
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.d.cts +2 -2
  6. package/dist/bundle/index.d.ts +2 -2
  7. package/dist/bundle/index.js +3 -3
  8. package/dist/{chunk-R2ZTGEVP.js → chunk-2CSJGFCB.js} +2 -2
  9. package/dist/{chunk-TOQK4KAN.js → chunk-4PWAI7Q4.js} +3 -3
  10. package/dist/{chunk-HC7Z5EQZ.js → chunk-AVVPZ4BC.js} +2 -2
  11. package/dist/{chunk-WN6UK7PM.js → chunk-EXHNQEV4.js} +2 -2
  12. package/dist/{chunk-2WGMYBYS.js → chunk-MDDTIZUO.js} +3 -3
  13. package/dist/{chunk-RSPLI376.js → chunk-PTVMYYON.js} +2 -2
  14. package/dist/{chunk-Y4CMTMUW.js → chunk-QAVUREFT.js} +2 -2
  15. package/dist/{chunk-7XBQS42M.js → chunk-QGZRWRSL.js} +2 -2
  16. package/dist/{chunk-PJK6IOBC.js → chunk-RKJ6OL7K.js} +1 -1
  17. package/dist/{chunk-PJK6IOBC.js.map → chunk-RKJ6OL7K.js.map} +1 -1
  18. package/dist/{chunk-YVFTBQHL.js → chunk-WDM5XGGS.js} +39 -2
  19. package/dist/chunk-WDM5XGGS.js.map +1 -0
  20. package/dist/consent/index.d.cts +2 -2
  21. package/dist/consent/index.d.ts +2 -2
  22. package/dist/{dev-unlock-BygpnIWe.d.ts → dev-unlock-BdPp68qn.d.ts} +1 -1
  23. package/dist/{dev-unlock-BZKx666y.d.cts → dev-unlock-Da1B0TIK.d.cts} +1 -1
  24. package/dist/{hash-CIyfmKsg.d.cts → hash-BEfzPKwo.d.cts} +1 -1
  25. package/dist/{hash-B0eU2Qv9.d.ts → hash-lsoL3eEW.d.ts} +1 -1
  26. package/dist/history/index.cjs.map +1 -1
  27. package/dist/history/index.d.cts +3 -3
  28. package/dist/history/index.d.ts +3 -3
  29. package/dist/history/index.js +2 -2
  30. package/dist/i18n/index.cjs.map +1 -1
  31. package/dist/i18n/index.d.cts +2 -2
  32. package/dist/i18n/index.d.ts +2 -2
  33. package/dist/i18n/index.js +3 -3
  34. package/dist/{index-Dp4tKCjX.d.ts → index-8QDuznDr.d.ts} +1 -1
  35. package/dist/{index-DsVbTDZI.d.cts → index-CywCC1qZ.d.cts} +1 -1
  36. package/dist/index.cjs +206 -2
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +5 -5
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.js +182 -13
  41. package/dist/index.js.map +1 -1
  42. package/dist/{ledger-UQIMMKO5.js → ledger-QZTTHQAQ.js} +3 -3
  43. package/dist/periods/index.cjs.map +1 -1
  44. package/dist/periods/index.d.cts +2 -2
  45. package/dist/periods/index.d.ts +2 -2
  46. package/dist/periods/index.js +3 -3
  47. package/dist/{public-envelope-3QTQADDW.js → public-envelope-6JTACYJV.js} +3 -3
  48. package/dist/session/index.d.cts +3 -3
  49. package/dist/session/index.d.ts +3 -3
  50. package/dist/shadow/index.d.cts +2 -2
  51. package/dist/shadow/index.d.ts +2 -2
  52. package/dist/store/index.d.cts +2 -2
  53. package/dist/store/index.d.ts +2 -2
  54. package/dist/sync/index.cjs.map +1 -1
  55. package/dist/sync/index.d.cts +1 -1
  56. package/dist/sync/index.d.ts +1 -1
  57. package/dist/sync/index.js +2 -2
  58. package/dist/team/index.cjs.map +1 -1
  59. package/dist/team/index.d.cts +2 -2
  60. package/dist/team/index.d.ts +2 -2
  61. package/dist/team/index.js +4 -4
  62. package/dist/tx/index.d.cts +2 -2
  63. package/dist/tx/index.d.ts +2 -2
  64. package/dist/{types-arFMsCtn.d.cts → types-Bnb82f5R.d.cts} +176 -4
  65. package/dist/{types-DD9eKKNc.d.ts → types-Bo7NSXJr.d.ts} +176 -4
  66. package/package.json +1 -1
  67. package/dist/chunk-YVFTBQHL.js.map +0 -1
  68. /package/dist/{chunk-R2ZTGEVP.js.map → chunk-2CSJGFCB.js.map} +0 -0
  69. /package/dist/{chunk-TOQK4KAN.js.map → chunk-4PWAI7Q4.js.map} +0 -0
  70. /package/dist/{chunk-HC7Z5EQZ.js.map → chunk-AVVPZ4BC.js.map} +0 -0
  71. /package/dist/{chunk-WN6UK7PM.js.map → chunk-EXHNQEV4.js.map} +0 -0
  72. /package/dist/{chunk-2WGMYBYS.js.map → chunk-MDDTIZUO.js.map} +0 -0
  73. /package/dist/{chunk-RSPLI376.js.map → chunk-PTVMYYON.js.map} +0 -0
  74. /package/dist/{chunk-Y4CMTMUW.js.map → chunk-QAVUREFT.js.map} +0 -0
  75. /package/dist/{chunk-7XBQS42M.js.map → chunk-QGZRWRSL.js.map} +0 -0
  76. /package/dist/{ledger-UQIMMKO5.js.map → ledger-QZTTHQAQ.js.map} +0 -0
  77. /package/dist/{public-envelope-3QTQADDW.js.map → public-envelope-6JTACYJV.js.map} +0 -0
@@ -1,5 +1,5 @@
1
- import { I as I18nStrategy } from '../types-arFMsCtn.cjs';
2
- export { D as DICT_COLLECTION_PREFIX, a as DictEntry, b as DictKeyDescriptor, c as DictionaryHandle, d as DictionaryOptions, e as I18nTextDescriptor, f as I18nTextOptions, g as applyI18nLocale, h as dictCollectionName, i as dictKey, j as i18nText, k as isDictCollectionName, l as isDictKeyDescriptor, m as isI18nTextDescriptor, r as resolveI18nText, v as validateI18nTextValue } from '../types-arFMsCtn.cjs';
1
+ import { I as I18nStrategy } from '../types-Bnb82f5R.cjs';
2
+ export { D as DICT_COLLECTION_PREFIX, a as DictEntry, b as DictKeyDescriptor, c as DictionaryHandle, d as DictionaryOptions, e as I18nTextDescriptor, f as I18nTextOptions, g as applyI18nLocale, h as dictCollectionName, i as dictKey, j as i18nText, k as isDictCollectionName, l as isDictKeyDescriptor, m as isI18nTextDescriptor, r as resolveI18nText, v as validateI18nTextValue } from '../types-Bnb82f5R.cjs';
3
3
  import '../lazy-builder-CZVLKh0Z.cjs';
4
4
  import '../predicate-SBHmi6D0.cjs';
5
5
  import '../strategy-D-SrOLCl.cjs';
@@ -1,5 +1,5 @@
1
- import { I as I18nStrategy } from '../types-DD9eKKNc.js';
2
- export { D as DICT_COLLECTION_PREFIX, a as DictEntry, b as DictKeyDescriptor, c as DictionaryHandle, d as DictionaryOptions, e as I18nTextDescriptor, f as I18nTextOptions, g as applyI18nLocale, h as dictCollectionName, i as dictKey, j as i18nText, k as isDictCollectionName, l as isDictKeyDescriptor, m as isI18nTextDescriptor, r as resolveI18nText, v as validateI18nTextValue } from '../types-DD9eKKNc.js';
1
+ import { I as I18nStrategy } from '../types-Bo7NSXJr.js';
2
+ export { D as DICT_COLLECTION_PREFIX, a as DictEntry, b as DictKeyDescriptor, c as DictionaryHandle, d as DictionaryOptions, e as I18nTextDescriptor, f as I18nTextOptions, g as applyI18nLocale, h as dictCollectionName, i as dictKey, j as i18nText, k as isDictCollectionName, l as isDictKeyDescriptor, m as isI18nTextDescriptor, r as resolveI18nText, v as validateI18nTextValue } from '../types-Bo7NSXJr.js';
3
3
  import '../lazy-builder-BwEoBQZ9.js';
4
4
  import '../predicate-SBHmi6D0.js';
5
5
  import '../strategy-D-SrOLCl.js';
@@ -10,10 +10,10 @@ import {
10
10
  isI18nTextDescriptor,
11
11
  resolveI18nText,
12
12
  validateI18nTextValue
13
- } from "../chunk-2WGMYBYS.js";
14
- import "../chunk-YVFTBQHL.js";
13
+ } from "../chunk-MDDTIZUO.js";
14
+ import "../chunk-WDM5XGGS.js";
15
15
  import "../chunk-CIMZBAZB.js";
16
- import "../chunk-PJK6IOBC.js";
16
+ import "../chunk-RKJ6OL7K.js";
17
17
  import "../chunk-MR4424N3.js";
18
18
  import "../chunk-ACLDOTNQ.js";
19
19
 
@@ -1,4 +1,4 @@
1
- import { aS as PublicEnvelope, b0 as BundleRecipient, aY as Vault } from './types-DD9eKKNc.js';
1
+ import { aS as PublicEnvelope, b0 as BundleRecipient, aY as Vault } from './types-Bo7NSXJr.js';
2
2
 
3
3
  /**
4
4
  * `.noydb` container format — byte layout, header schema, validators.
@@ -1,4 +1,4 @@
1
- import { aS as PublicEnvelope, b0 as BundleRecipient, aY as Vault } from './types-arFMsCtn.cjs';
1
+ import { aS as PublicEnvelope, b0 as BundleRecipient, aY as Vault } from './types-Bnb82f5R.cjs';
2
2
 
3
3
  /**
4
4
  * `.noydb` container format — byte layout, header schema, validators.
package/dist/index.cjs CHANGED
@@ -5012,6 +5012,11 @@ function canRevoke(callerRole, targetRole) {
5012
5012
  if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
5013
5013
  return false;
5014
5014
  }
5015
+ function canUpdateRole(callerRole, targetRole) {
5016
+ if (callerRole === "owner") return true;
5017
+ if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
5018
+ return false;
5019
+ }
5015
5020
  async function loadKeyring(adapter, vault, userId, passphrase) {
5016
5021
  const envelope = await adapter.get(vault, "_keyring", userId);
5017
5022
  if (!envelope) {
@@ -5205,6 +5210,37 @@ async function revoke(adapter, vault, callerKeyring, options) {
5205
5210
  await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections]);
5206
5211
  }
5207
5212
  }
5213
+ async function updateKeyringIdentity(adapter, vault, callerKeyring, options) {
5214
+ if (options.role === void 0 && options.displayName === void 0 && options.permissions === void 0) {
5215
+ throw new ValidationError(
5216
+ `updateUser: at least one of role / displayName / permissions must be provided (userId: "${options.userId}").`
5217
+ );
5218
+ }
5219
+ const env = await adapter.get(vault, "_keyring", options.userId);
5220
+ if (!env) {
5221
+ throw new NoAccessError(
5222
+ `updateUser: user "${options.userId}" has no keyring in vault "${vault}".`
5223
+ );
5224
+ }
5225
+ const target = JSON.parse(env._data);
5226
+ if (!canUpdateRole(callerKeyring.role, target.role)) {
5227
+ throw new PermissionDeniedError(
5228
+ `Role "${callerKeyring.role}" cannot update a keyring with role "${target.role}"`
5229
+ );
5230
+ }
5231
+ if (options.role !== void 0 && options.role !== target.role && !canUpdateRole(callerKeyring.role, options.role)) {
5232
+ throw new PermissionDeniedError(
5233
+ `Role "${callerKeyring.role}" cannot promote target to role "${options.role}"`
5234
+ );
5235
+ }
5236
+ const next = {
5237
+ ...target,
5238
+ ...options.role !== void 0 && { role: options.role },
5239
+ ...options.displayName !== void 0 && { display_name: options.displayName },
5240
+ ...options.permissions !== void 0 && { permissions: options.permissions }
5241
+ };
5242
+ await writeKeyringFile(adapter, vault, options.userId, next);
5243
+ }
5208
5244
  async function rotateKeys(adapter, vault, callerKeyring, collections) {
5209
5245
  const newDeks = /* @__PURE__ */ new Map();
5210
5246
  for (const collName of collections) {
@@ -5505,6 +5541,38 @@ async function enrollAuthenticator(store, vault, keyring, options) {
5505
5541
  await persistKeyring(store, vault, next);
5506
5542
  return next;
5507
5543
  }
5544
+ async function updateAuthenticator(store, vault, keyring, slotId, options) {
5545
+ if (options.meta === void 0) {
5546
+ throw new ValidationError(
5547
+ `updateAuthenticator: at least one of meta must be provided (slotId: "${slotId}").`
5548
+ );
5549
+ }
5550
+ const idx = keyring.authenticators.findIndex((a) => a.id === slotId);
5551
+ if (idx === -1) {
5552
+ throw new NoAccessError(
5553
+ `updateAuthenticator: slot "${slotId}" not found in vault "${vault}".`
5554
+ );
5555
+ }
5556
+ const existing = keyring.authenticators[idx];
5557
+ const mergedMeta = { ...existing.meta };
5558
+ for (const [k, v] of Object.entries(options.meta)) {
5559
+ if (v === void 0) continue;
5560
+ if (v === null) {
5561
+ delete mergedMeta[k];
5562
+ continue;
5563
+ }
5564
+ mergedMeta[k] = v;
5565
+ }
5566
+ const next = { ...existing, meta: mergedMeta };
5567
+ const nextSlots = [...keyring.authenticators];
5568
+ nextSlots[idx] = next;
5569
+ const nextKeyring = {
5570
+ ...keyring,
5571
+ authenticators: nextSlots
5572
+ };
5573
+ await persistKeyring(store, vault, nextKeyring);
5574
+ return nextKeyring;
5575
+ }
5508
5576
  async function removeAuthenticator(store, vault, keyring, slotId) {
5509
5577
  const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
5510
5578
  if (filtered.length === keyring.authenticators.length) {
@@ -5959,6 +6027,17 @@ var UserApi = class {
5959
6027
  * the envelope on first call. Optimistic-concurrency safe — a stale
5960
6028
  * `_v` (parallel writer on another device) throws `ConflictError`.
5961
6029
  *
6030
+ * Patch semantics (#57):
6031
+ * - `undefined` (or omitted key) — skip; existing value preserved
6032
+ * - `null` — delete the field from the merged result
6033
+ * - any other value — overwrite (deep-merge for plain objects,
6034
+ * replace for primitives / arrays)
6035
+ *
6036
+ * To clear a field, pass `null` rather than `undefined`. Callers
6037
+ * with shape `T = string | null` where `null` is a meaningful value
6038
+ * should use `setMe` for that specific field instead — `null` here
6039
+ * always means delete.
6040
+ *
5962
6041
  * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
5963
6042
  * Pass `presented` to satisfy tightened policies that require a
5964
6043
  * factor proof (e.g. STRICT_POLICY's TOTP requirement).
@@ -6130,9 +6209,17 @@ function deepMerge(source, patch) {
6130
6209
  }
6131
6210
  const out = { ...source };
6132
6211
  for (const [key, patchVal] of Object.entries(patch)) {
6212
+ if (patchVal === void 0) {
6213
+ continue;
6214
+ }
6215
+ if (patchVal === null) {
6216
+ delete out[key];
6217
+ continue;
6218
+ }
6133
6219
  const sourceVal = source[key];
6134
- if (isPlainObject(sourceVal) && isPlainObject(patchVal)) {
6135
- out[key] = deepMerge(sourceVal, patchVal);
6220
+ if (isPlainObject(patchVal)) {
6221
+ const recurseSource = isPlainObject(sourceVal) ? sourceVal : {};
6222
+ out[key] = deepMerge(recurseSource, patchVal);
6136
6223
  } else {
6137
6224
  out[key] = patchVal;
6138
6225
  }
@@ -13656,6 +13743,12 @@ var PERSONAL_POLICY = Object.freeze({
13656
13743
  },
13657
13744
  "enroll-authenticator": { minTier: 1 },
13658
13745
  "remove-authenticator": { minTier: 1 },
13746
+ // update-authenticator: meta-only mutation (slot rename, label
13747
+ // changes). Symmetric with enroll/remove under PERSONAL — tier-1
13748
+ // unlock alone. The structural anti-slot-swap guard inside the
13749
+ // implementation enforces wrap-material/id/method immutability
13750
+ // regardless of this gate's settings.
13751
+ "update-authenticator": { minTier: 1 },
13659
13752
  "rotate-unlock": { minTier: 2 },
13660
13753
  "enroll-user": { minTier: 1 },
13661
13754
  "revoke-user": { minTier: 1 },
@@ -13665,6 +13758,12 @@ var PERSONAL_POLICY = Object.freeze({
13665
13758
  // virtue of being a co-owner). Tier-1 unlock is the floor; the
13666
13759
  // STRICT preset adds a recovery/email-OTP requirement.
13667
13760
  "peer-recover-user": { minTier: 1 },
13761
+ // update-user: post-grant identity mutation (role/displayName/
13762
+ // permissions). PERSONAL_POLICY treats this on par with enroll-user
13763
+ // / revoke-user — tier-1 unlock alone. The role-elevation guard
13764
+ // inside the implementation is the structural backstop that this
13765
+ // gate's settings cannot weaken.
13766
+ "update-user": { minTier: 1 },
13668
13767
  "export-bundle": { minTier: 1 },
13669
13768
  "export-plaintext": {
13670
13769
  minTier: 1,
@@ -13709,6 +13808,15 @@ var STRICT_POLICY = Object.freeze({
13709
13808
  minTier: 1,
13710
13809
  factors: [{ anyOf: ["totp", "email-otp"] }]
13711
13810
  },
13811
+ // STRICT update-authenticator: same factor floor as enroll/remove.
13812
+ // Even though meta changes don't touch wrap material, a malicious
13813
+ // rename could mislead the user about which device a slot
13814
+ // corresponds to ("MacBook Touch ID" → "iPhone Touch ID" on a
13815
+ // shared workstation). STRICT requires a fresh factor proof.
13816
+ "update-authenticator": {
13817
+ minTier: 1,
13818
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13819
+ },
13712
13820
  "rotate-unlock": { minTier: 1 },
13713
13821
  "enroll-user": {
13714
13822
  minTier: 1,
@@ -13731,6 +13839,18 @@ var STRICT_POLICY = Object.freeze({
13731
13839
  minTier: 1,
13732
13840
  factors: [{ anyOf: ["recovery", "totp", "email-otp", "webauthn-roaming"] }]
13733
13841
  },
13842
+ // STRICT update-user: matches the enroll-user / revoke-user shape
13843
+ // (off-device factor required). Update-user is admin-shaped — it
13844
+ // mutates someone else's role/permissions; STRICT requires a fresh
13845
+ // off-device factor proof so the operator affirmatively re-asserts
13846
+ // identity at the moment of mutation. Platform-bound factors
13847
+ // (Touch ID / password / PIN) intentionally excluded: same logic as
13848
+ // peer-recover-user — the off-device requirement is the whole
13849
+ // point under STRICT.
13850
+ "update-user": {
13851
+ minTier: 1,
13852
+ factors: [{ anyOf: ["totp", "email-otp"] }]
13853
+ },
13734
13854
  "export-bundle": {
13735
13855
  minTier: 1,
13736
13856
  factors: [{ anyOf: ["totp", "email-otp"] }],
@@ -14125,6 +14245,56 @@ var Noydb = class {
14125
14245
  const keyring = await this.getKeyring(vault);
14126
14246
  await revoke(this.options.store, vault, keyring, options);
14127
14247
  }
14248
+ /**
14249
+ * Mutate post-grant identity fields on an existing keyring — `role`,
14250
+ * `displayName`, and/or `permissions`. Pure plaintext-header rewrite:
14251
+ * no DEK rewrap, no KEK required, no authenticator slots touched.
14252
+ * Tier-2 enrollments and recovery codes survive.
14253
+ *
14254
+ * Different from `db.revoke + db.grant`:
14255
+ *
14256
+ * - Same `userId`, same DEK wrappings, same `granted_by`, same
14257
+ * `_users/<keyringId>` envelope. Only the specified header
14258
+ * fields move. Last-write-wins via the standard keyring put.
14259
+ * - No cascade on role demotion (admins demoted to operator keep
14260
+ * the keyrings they previously granted; the cascade rules are
14261
+ * a `db.revoke` concern, not `db.updateUser`).
14262
+ * - Tier-2 slots NOT dropped — the wrapping is unaffected.
14263
+ *
14264
+ * Role-elevation guard: BOTH the old and new role must satisfy
14265
+ * `db.grant`'s hierarchy. Owner can do anything; admin manages
14266
+ * admin/operator/viewer/client laterally; admin cannot promote to
14267
+ * owner OR demote from owner. The guard runs regardless of the
14268
+ * `update-user` policy gate's settings — gates can only be more
14269
+ * permissive than the structural floor, never less.
14270
+ *
14271
+ * Gated by `update-user`. `STRICT_POLICY` requires a TOTP/email-OTP
14272
+ * factor proof so the operator affirmatively re-asserts identity at
14273
+ * the moment of mutation; `PERSONAL_POLICY` accepts a tier-1 unlock
14274
+ * alone.
14275
+ *
14276
+ * ```ts
14277
+ * await db.updateUser('acme', {
14278
+ * userId: 'bob',
14279
+ * role: 'operator', // promote
14280
+ * permissions: { invoices: 'rw' },
14281
+ * }, { factors: [{ kind: 'totp' }] })
14282
+ * ```
14283
+ *
14284
+ * @throws `NoAccessError` when no keyring exists for the target.
14285
+ * @throws `PermissionDeniedError` when the role hierarchy rejects.
14286
+ * @throws `ValidationError` when no field is provided.
14287
+ *
14288
+ * @see #54
14289
+ */
14290
+ async updateUser(vault, options, factors) {
14291
+ await this.checkGate(vault, "update-user", factors);
14292
+ const keyring = await this.getKeyring(vault);
14293
+ await updateKeyringIdentity(this.options.store, vault, keyring, options);
14294
+ if (options.userId === this.options.user) {
14295
+ this.keyringCache.delete(vault);
14296
+ }
14297
+ }
14128
14298
  /**
14129
14299
  * Rotate the DEKs for the given collections in a vault.
14130
14300
  *
@@ -14668,6 +14838,40 @@ var Noydb = class {
14668
14838
  const keyring = await this.getKeyring(vault);
14669
14839
  return keyring.authenticators;
14670
14840
  }
14841
+ /**
14842
+ * Mutate the `meta` blob on an existing authenticator slot — slot
14843
+ * rename, label change, attachment of UI hints. The slot's `id`,
14844
+ * `method`, and wrap material (`wrapped_kek` / `wrapped_deks` + `iv`)
14845
+ * are immutable through this method. Anti-slot-swap is structural,
14846
+ * not gate-driven.
14847
+ *
14848
+ * `meta` patch semantics (#57-aligned):
14849
+ * - Top-level merge — absent keys preserved
14850
+ * - `null` value — delete that meta key
14851
+ * - Other values — replace verbatim
14852
+ *
14853
+ * Use case: per-slot nickname for "iPhone Touch ID" vs "MacBook
14854
+ * Touch ID" disambiguation in admin UIs. The slot id (auto-derived
14855
+ * from credentialId prefix) is not human-friendly; `meta.nickname`
14856
+ * is.
14857
+ *
14858
+ * Gated by `update-authenticator`. PERSONAL_POLICY: tier-1 unlock
14859
+ * alone (matches enroll/remove). STRICT_POLICY: tier-1 +
14860
+ * TOTP/email-OTP factor proof — a malicious rename on a shared
14861
+ * workstation could mislead the user about which device a slot
14862
+ * corresponds to, so STRICT requires fresh factor binding.
14863
+ *
14864
+ * @throws `NoAccessError` when no slot with the given id exists.
14865
+ * @throws `ValidationError` when no patch field is provided.
14866
+ *
14867
+ * @see #55
14868
+ */
14869
+ async updateAuthenticator(vault, slotId, options, presented) {
14870
+ await this.checkGate(vault, "update-authenticator", presented);
14871
+ const keyring = await this.getKeyring(vault);
14872
+ const next = await updateAuthenticator(this.options.store, vault, keyring, slotId, options);
14873
+ this.keyringCache.set(vault, next);
14874
+ }
14671
14875
  /**
14672
14876
  * Native WebAuthn enrollment using the **real** internal keyring (#16).
14673
14877
  *