@cortexkit/common-auth 0.4.1 → 0.4.3

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.
@@ -1,7 +1,7 @@
1
1
  import { PoolOperationError } from './errors.js';
2
2
  import { toFailure, withTransaction } from './mutate.js';
3
3
  import { LockStack } from './refresh-lock.js';
4
- import { refusal, unknownRow } from './runtime.js';
4
+ import { refusal, requireBound, unknownRow, } from './runtime.js';
5
5
  import { isCredentialEpoch } from './schema.js';
6
6
  /**
7
7
  * Merges a quota observation into a row's stored map under the store locks,
@@ -26,6 +26,7 @@ export async function recordQuota(rt, id, attribution, observation) {
26
26
  // has no such triple on disk, so no reading is recorded for it.
27
27
  if (!row.credential)
28
28
  throw refusal('pull', id, 'no-credential', `row ${id} holds no credential`);
29
+ requireBound('pull', row);
29
30
  const entry = tx.entry(id);
30
31
  if (row.torn ||
31
32
  !entry ||
@@ -19,7 +19,7 @@ export type PoolFailurePhase = 'before-first-write' | 'after-first-write' | 'pul
19
19
  * lock outcomes (a wait that ran out, and a lease found lost); the rest are
20
20
  * refusals and failures of the operation itself.
21
21
  */
22
- export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
22
+ export type PoolFailureKind = 'lock-contention' | 'lock-ownership' | 'pending-migration' | 'load-error' | 'unknown-row' | 'invalid-row' | 'invalid-input' | 'id-exists' | 'id-removed' | 'type-mismatch' | 'no-credential' | 'row-disabled' | 'row-protected' | 'duplicate-identity' | 'identity-mismatch' | 'endpoint-mismatch' | 'row-key-changed' | 'invalid-order' | 'refresh-stamp-ahead' | 'unbound-credential' | 'attribution' | 'provider' | 'pull' | 'invalid-quota' | 'after-persist-hook' | 'unexpected';
23
23
  /**
24
24
  * The single failure value of every store operation. `committed` is present
25
25
  * only when the operation had already written a credential to the state file
@@ -12,7 +12,7 @@ export type { LockEvent, PoolLockOptions, PoolLockSpec, } from './refresh-lock.j
12
12
  export { POOL_LOCK_DEFAULTS } from './refresh-lock.js';
13
13
  export type { AddInput, AddResult, FailureHook, RemoveOptions, RemoveResult, RemoveView, ReorderOptions, ReorderResult, RowOperationOptions, RowToggleOptions, } from './rows.js';
14
14
  export type { PullReason } from './runtime.js';
15
- export type { ApiKeyCredential, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
15
+ export type { ApiKeyCredential, CredentialStampStatus, OAuthCredential, PoolCredential, PoolRow, QuotaCodec, RotateCredential, StoredCredential, } from './schema.js';
16
16
  export { fingerprintOf, LEGACY_STORE_VERSION, POOL_KEY, POOL_ROWS_KEY, POOL_SCHEMA_VERSION, REFRESH_STAMP_TOLERANCE_MS, rowLockKey, } from './schema.js';
17
17
  export type { PoolSettings, SettingsMutator, SettingsRead, UpdateSettingsOptions, UpdateSettingsResult, } from './settings.js';
18
18
  export { POOL_OWNED_KEYS } from './settings.js';
@@ -23,6 +23,12 @@ export interface StoreContext {
23
23
  hold?: (point: HoldPoint, rowId: string) => void | Promise<void>;
24
24
  /** Ids whose per-row entry a library write dropped in this process. */
25
25
  removedIds: Set<string>;
26
+ /**
27
+ * When true, rows whose credential stamp is not bound load `unbound` and
28
+ * the operations that use a credential refuse them (see
29
+ * `OpenPoolStoreOptions.requireCredentialStamps`).
30
+ */
31
+ requireCredentialStamps?: boolean;
26
32
  }
27
33
  export interface Snapshot {
28
34
  configExists: boolean;
@@ -39,7 +39,9 @@ export async function readPool(ctx) {
39
39
  stateExists: state.exists,
40
40
  config: config.config,
41
41
  state: state.state,
42
- rows: loadRows(config.config, state.state, ctx.codec),
42
+ rows: loadRows(config.config, state.state, ctx.codec, {
43
+ requireCredentialStamps: ctx.requireCredentialStamps === true,
44
+ }),
43
45
  };
44
46
  }
45
47
  /** The refusal for a pool that is not ready, as a failure value. */
@@ -86,7 +88,9 @@ export class Transaction {
86
88
  * `PoolRow.torn`).
87
89
  */
88
90
  rows() {
89
- return loadRows(this.config, this.state, this.ctx.codec);
91
+ return loadRows(this.config, this.state, this.ctx.codec, {
92
+ requireCredentialStamps: this.ctx.requireCredentialStamps === true,
93
+ });
90
94
  }
91
95
  row(id) {
92
96
  return this.rows().find((row) => row.id === id);
@@ -14,6 +14,19 @@ export interface OpenPoolStoreOptions {
14
14
  configPath: string;
15
15
  statePath: string;
16
16
  quota: QuotaCodec;
17
+ /**
18
+ * Refuse every credential this store did not stamp (default false, which
19
+ * loads unstamped and mis-stamped credentials as older writers left them).
20
+ * When true, a row whose `stamp` is not `bound` loads `unbound` and is no
21
+ * candidate, and every operation that would use or keep its credential or
22
+ * what was observed about it (`refresh`, quota pulls, `recordQuota`,
23
+ * `recordIdentity`, `rotate`, and an `add` of the same secret or onto the
24
+ * same credential-less row) refuses with `unbound-credential` before any
25
+ * provider call or write, checked again under the locks and at commit.
26
+ * Nothing makes such a row bound except `replace`, which starts a new
27
+ * credential epoch and drops what was observed about the old one.
28
+ */
29
+ requireCredentialStamps?: boolean;
17
30
  /** Injected clock for leases, refresh stamps and `addedAt`. */
18
31
  now?: () => number;
19
32
  /**
@@ -59,6 +59,7 @@ export function openPoolStore(options) {
59
59
  ...(options.onLockStep ? { onLockStep: options.onLockStep } : {}),
60
60
  },
61
61
  removedIds: memory.removedIds,
62
+ requireCredentialStamps: options.requireCredentialStamps === true,
62
63
  ...(options.logger ? { logger: options.logger } : {}),
63
64
  ...(options.onStep ? { onStep: options.onStep } : {}),
64
65
  ...(options.hold ? { hold: options.hold } : {}),
@@ -1,6 +1,6 @@
1
1
  import { PoolOperationError } from './errors.js';
2
2
  import type { PoolLogger } from './hooks.js';
3
- import type { PullReason, StoreRuntime } from './runtime.js';
3
+ import { type PullReason, type StoreRuntime } from './runtime.js';
4
4
  import type { StoredCredential } from './schema.js';
5
5
  /** What a pull is issued with: the credential and its attribution tuple. */
6
6
  export interface PullRequest {
@@ -2,6 +2,7 @@ import { recordQuota } from './attribution.js';
2
2
  import { PoolOperationError } from './errors.js';
3
3
  import { toFailure, withTransaction } from './mutate.js';
4
4
  import { LockStack } from './refresh-lock.js';
5
+ import { requireBound } from './runtime.js';
5
6
  /**
6
7
  * Fires quota pulls without ever making a caller wait for one. A pull first
7
8
  * gives a row without a per-row entry its entry at epoch 1 (its own locked
@@ -50,6 +51,14 @@ export class PullScheduler {
50
51
  try {
51
52
  const request = await withTransaction(ctx, locks, progress, { operation: 'pull', rowId: id }, async (tx) => {
52
53
  const row = tx.row(id);
54
+ // A row that would pull but for its unbound credential refuses, so
55
+ // the failure hook hears of it instead of the pull vanishing.
56
+ if (row?.unbound &&
57
+ row.enabled &&
58
+ row.type === 'oauth' &&
59
+ row.credential &&
60
+ !row.invalid)
61
+ requireBound('pull', row);
53
62
  // Disabled, API-key and credential-less rows never pull.
54
63
  if (!row?.candidate || row.type !== 'oauth')
55
64
  return undefined;
@@ -3,7 +3,7 @@ import { assertNotInsideHook, runInsideHook } from './hooks.js';
3
3
  import { recordIdentityIn } from './identity.js';
4
4
  import { runOperation, withTransaction } from './mutate.js';
5
5
  import { rotateIn } from './rows.js';
6
- import { readRow, refusal, rowLockSpec } from './runtime.js';
6
+ import { readRow, refusal, requireBound, rowLockSpec, } from './runtime.js';
7
7
  import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
8
8
  function requireRefreshable(id, row) {
9
9
  if (!row)
@@ -47,6 +47,7 @@ export async function refreshRow(rt, id, provider, options = {}) {
47
47
  const captureProgress = { writes: 0 };
48
48
  const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
49
49
  let row = requireRefreshable(id, tx.row(id));
50
+ requireBound('refresh', row);
50
51
  if (!row.hasEntry) {
51
52
  tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
52
53
  await tx.commitConfig();
@@ -113,6 +114,10 @@ export async function refreshRow(rt, id, provider, options = {}) {
113
114
  kind: 'attribution',
114
115
  message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
115
116
  });
117
+ // The epoch fence above does not see a credential another writer
118
+ // swapped in under the same epoch; the stamp does, and committing
119
+ // the rotation would stamp the swapped row as bound.
120
+ requireBound('refresh', current);
116
121
  const reason = await options.refuse?.(current);
117
122
  if (reason !== undefined)
118
123
  return { refused: reason };
@@ -2,7 +2,7 @@ import { PoolOperationError } from './errors.js';
2
2
  import { assertNotInsideHook, runInsideHook } from './hooks.js';
3
3
  import { DUPLICATE_IDENTITY_REASON, disableIdentityDuplicates, disableIn, recordIdentityIn, } from './identity.js';
4
4
  import { notReadyError, readPool, runOperation, withTransaction, } from './mutate.js';
5
- import { readRow, refusal, rowLockSpec, unknownRow, } from './runtime.js';
5
+ import { readRow, refusal, requireBound, rowLockSpec, unknownRow, } from './runtime.js';
6
6
  import { CREDENTIAL_STAMP_KEY, credentialProblem, fingerprintOf, idProblem, isCredentialEpoch, isRecord, rosterRowFor, rotationStamp, rowLockKey, stampFor, stateFieldsFor, storedCredential, } from './schema.js';
7
7
  import { bindReplacement } from './torn.js';
8
8
  /** Fields of a state entry that belong to the credential it replaces. */
@@ -129,6 +129,11 @@ export async function addRow(rt, input, options = {}) {
129
129
  const fingerprint = fingerprintOf(credential);
130
130
  const same = rows.find((row) => row.invalid === undefined && row.fingerprint === fingerprint);
131
131
  if (same) {
132
+ // Re-adding a secret whose stamp is not proved would rotate it in
133
+ // and keep the identity and quota recorded beside it, making that
134
+ // unproved record look bound. The add is refused instead, and the
135
+ // caller replaces the row, which starts a new credential epoch.
136
+ requireBound('add', same);
132
137
  // The same secret is the same credential, so re-adding it rotates
133
138
  // that row. An identity or endpoint given with it must match the
134
139
  // row's; a different one is refused rather than silently replaced
@@ -146,6 +151,10 @@ export async function addRow(rt, input, options = {}) {
146
151
  throw refusal('add', id, 'invalid-row', `row ${id} is invalid`);
147
152
  if (existing.credential)
148
153
  throw refusal('add', id, 'id-exists', `row ${id} already holds a credential`);
154
+ // Completing a credential-less row keeps its epoch, identity and
155
+ // quota; when stamps are required those belong to no proved
156
+ // credential, so the row must be replaced instead.
157
+ requireBound('add', existing);
149
158
  if (existing.type !== credential.type)
150
159
  throw refusal('add', id, 'type-mismatch', `row ${id} is a ${existing.type} row`);
151
160
  if (identity !== undefined &&
@@ -275,6 +284,9 @@ export async function rotateRow(rt, id, credential, input = {}, options = {}) {
275
284
  const row = requireUsableRow('rotate', id, tx.row(id), credential);
276
285
  if (rowLockKey(row) !== rowLockKey(seen))
277
286
  throw keyChanged('rotate', id);
287
+ // A rotation keeps the row's epoch, identity and quota, so it must
288
+ // never be what stamps an unproved row as bound.
289
+ requireBound('rotate', row);
278
290
  // A rotation stays with one account: it may record the first
279
291
  // identity the row learns, but a credential of another known
280
292
  // account is a replacement (new epoch, quota and errors dropped).
@@ -553,6 +565,7 @@ export async function recordRowIdentity(rt, id, identity, attribution, options =
553
565
  const row = requireUsableRow('recordIdentity', id, tx.row(id));
554
566
  if (rowLockKey(row) !== rowLockKey(seen))
555
567
  throw keyChanged('recordIdentity', id);
568
+ requireBound('recordIdentity', row);
556
569
  if ((row.credentialEpoch ?? 1) !== captured)
557
570
  throw refusal('recordIdentity', id, 'attribution', `the identity for ${id} was looked up for a credential the row no longer holds`, true);
558
571
  if (row.identity !== undefined && row.identity !== identity)
@@ -25,4 +25,12 @@ export declare function readRow(rt: StoreRuntime, operation: PoolOperation, id:
25
25
  row: PoolRow;
26
26
  }>;
27
27
  export declare function unknownRow(operation: PoolOperation, id: string): PoolOperationError;
28
+ /**
29
+ * Refuses a row the store was told to distrust: opened with
30
+ * `requireCredentialStamps`, a row whose credential stamp is not bound loads
31
+ * `unbound` (see `PoolRow.unbound`). Called on every locked read of the row
32
+ * an operation acts on, before it calls a provider or writes, so a credential
33
+ * another writer swapped in while the operation waited is refused too.
34
+ */
35
+ export declare function requireBound(operation: PoolOperation, row: PoolRow): void;
28
36
  export declare function refusal(operation: PoolOperation, id: string, kind: PoolOperationError['kind'], message: string, retryable?: boolean): PoolOperationError;
@@ -33,6 +33,17 @@ export function unknownRow(operation, id) {
33
33
  message: `no row ${id} in the pool`,
34
34
  });
35
35
  }
36
+ /**
37
+ * Refuses a row the store was told to distrust: opened with
38
+ * `requireCredentialStamps`, a row whose credential stamp is not bound loads
39
+ * `unbound` (see `PoolRow.unbound`). Called on every locked read of the row
40
+ * an operation acts on, before it calls a provider or writes, so a credential
41
+ * another writer swapped in while the operation waited is refused too.
42
+ */
43
+ export function requireBound(operation, row) {
44
+ if (row.unbound)
45
+ throw refusal(operation, row.id, 'unbound-credential', `row ${row.id}'s credential is not the one this store stamped for it (stamp ${row.stamp}); replace it with fresh material`);
46
+ }
36
47
  export function refusal(operation, id, kind, message, retryable = false) {
37
48
  return new PoolOperationError({
38
49
  operation,
@@ -42,6 +42,21 @@ export interface QuotaCodec {
42
42
  validate(value: unknown): boolean;
43
43
  merge(stored: unknown | undefined, observation: unknown): unknown;
44
44
  }
45
+ /**
46
+ * What the stamp beside a row's credential proves (see `CredentialStamp`).
47
+ *
48
+ * `none`: the row holds no credential, so there is nothing to stamp.
49
+ * `bound`: the stamp was written with this credential (its digest matches),
50
+ * names the row's credential epoch (1 for a row without a per-row entry),
51
+ * and any identity or endpoint it records is the row's.
52
+ * `missing`: the credential carries no stamp (written by a writer that does
53
+ * not know about stamps, or one that dropped it).
54
+ * `malformed`: the stamp is not one this store writes (wrong shape, or an
55
+ * epoch outside the positive safe integers).
56
+ * `mismatched`: the stamp was written for another credential, epoch,
57
+ * identity or endpoint than the row now holds.
58
+ */
59
+ export type CredentialStampStatus = 'none' | 'bound' | 'missing' | 'malformed' | 'mismatched';
45
60
  /** One row of the pool as loaded. */
46
61
  export interface PoolRow {
47
62
  id: string;
@@ -74,6 +89,21 @@ export interface PoolRow {
74
89
  * the next store write on it writes the config to match.
75
90
  */
76
91
  torn?: true;
92
+ /**
93
+ * Whether the credential is the one the store last stamped for this row
94
+ * (see `CredentialStampStatus`). Every row the store loads carries it; a
95
+ * torn row reports the stamp of the row as it is shown completed. It is
96
+ * optional only so that rows built by hand (test fixtures) still type.
97
+ */
98
+ stamp?: CredentialStampStatus;
99
+ /**
100
+ * Set only when the store was opened with `requireCredentialStamps` and
101
+ * the row's `stamp` is not `bound`: the row is never a candidate, and
102
+ * `refresh`, quota pulls, `recordQuota`, `recordIdentity`, `rotate` and a
103
+ * re-`add` onto it refuse with `unbound-credential`. Only `replace` (or a
104
+ * new row) makes it usable again.
105
+ */
106
+ unbound?: true;
77
107
  }
78
108
  /**
79
109
  * Key, inside a state-file account entry, of the stamp naming what the
@@ -124,6 +124,45 @@ export function parseStamp(raw) {
124
124
  },
125
125
  };
126
126
  }
127
+ /**
128
+ * Whether a binding a stamp records is the row's: an identity it names must
129
+ * be the row's recorded identity, and an endpoint it names must be the one
130
+ * the row sends its API key to. A binding without an identity says none was
131
+ * known when it was written, so an identity learnt since does not contradict
132
+ * it.
133
+ */
134
+ function bindingAgrees(binding, credential, identity) {
135
+ if (binding.identity !== undefined && binding.identity !== identity)
136
+ return false;
137
+ if (binding.baseURL !== undefined &&
138
+ (credential.type !== 'api' || credential.baseURL !== binding.baseURL))
139
+ return false;
140
+ if (binding.authHeader !== undefined &&
141
+ (credential.type !== 'api' || credential.authHeader !== binding.authHeader))
142
+ return false;
143
+ return true;
144
+ }
145
+ /**
146
+ * The stamp status of a loaded row (see `CredentialStampStatus`).
147
+ * `credentialEpoch` is undefined only when the row's per-row entry exists but
148
+ * failed validation, in which case no stamp can match it.
149
+ */
150
+ function stampStatusOf(credential, credentialEpoch, identity, account) {
151
+ if (!credential)
152
+ return 'none';
153
+ if (!isRecord(account) || !Object.hasOwn(account, CREDENTIAL_STAMP_KEY))
154
+ return 'missing';
155
+ const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
156
+ if (!stamp)
157
+ return 'malformed';
158
+ if (stamp.digest !== credentialDigest(credential))
159
+ return 'mismatched';
160
+ if (stamp.credentialEpoch !== credentialEpoch)
161
+ return 'mismatched';
162
+ if (stamp.binding && !bindingAgrees(stamp.binding, credential, identity))
163
+ return 'mismatched';
164
+ return 'bound';
165
+ }
127
166
  export function classifyConfig(read) {
128
167
  if (!read.exists)
129
168
  return { status: 'ready', exists: false, config: {} };
@@ -302,6 +341,7 @@ export function buildRawRows(config, state, codec) {
302
341
  hasEntry: Object.hasOwn(entries, id),
303
342
  candidate: false,
304
343
  invalid: 'roster',
344
+ stamp: 'none',
305
345
  });
306
346
  continue;
307
347
  }
@@ -311,6 +351,9 @@ export function buildRawRows(config, state, codec) {
311
351
  const entry = hasEntry ? parseEntry(entries[id], codec) : undefined;
312
352
  const credential = credentialFor(raw, stateAccounts[id]);
313
353
  const enabled = raw.enabled !== false;
354
+ const identity = typeof raw.accountId === 'string' && raw.accountId
355
+ ? raw.accountId
356
+ : undefined;
314
357
  const row = {
315
358
  id,
316
359
  type,
@@ -320,9 +363,7 @@ export function buildRawRows(config, state, codec) {
320
363
  candidate: false,
321
364
  ...(typeof raw.label === 'string' ? { label: raw.label } : {}),
322
365
  ...(typeof raw.addedAt === 'number' ? { addedAt: raw.addedAt } : {}),
323
- ...(typeof raw.accountId === 'string' && raw.accountId
324
- ? { identity: raw.accountId }
325
- : {}),
366
+ ...(identity !== undefined ? { identity } : {}),
326
367
  ...(credential
327
368
  ? { credential, fingerprint: fingerprintOf(credential) }
328
369
  : {}),
@@ -331,6 +372,9 @@ export function buildRawRows(config, state, codec) {
331
372
  ? { disabledReason: entry.disabledReason }
332
373
  : {}),
333
374
  ...(entry && 'quota' in entry ? { quota: entry.quota } : {}),
375
+ // A row without a per-row entry is at credential epoch 1 (the epoch the
376
+ // store stamps and later gives it), so its stamp is checked against 1.
377
+ stamp: stampStatusOf(credential, entry ? entry.credentialEpoch : hasEntry ? undefined : 1, identity, stateAccounts[id]),
334
378
  };
335
379
  if (hasEntry && !entry) {
336
380
  row.invalid = 'entry';
@@ -25,5 +25,11 @@ export declare function completeTornRows(config: Record<string, unknown>, state:
25
25
  * The rows every reader gets. A torn row is shown completed (the identity,
26
26
  * endpoint and epoch its stamp names, beside the credential it stamps), is
27
27
  * marked `torn` and is never a candidate; every other row is as on disk.
28
+ * With `requireCredentialStamps`, a row whose stamp is not `bound` is marked
29
+ * `unbound` and is never a candidate either. A torn row is shown with the
30
+ * replacement's stamp, which binds the completed row, so it is not unbound:
31
+ * it stays out of routing only until its completion is written.
28
32
  */
29
- export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): PoolRow[];
33
+ export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec, options?: {
34
+ requireCredentialStamps?: boolean;
35
+ }): PoolRow[];
@@ -97,17 +97,23 @@ export function completeTornRows(config, state, codec) {
97
97
  * The rows every reader gets. A torn row is shown completed (the identity,
98
98
  * endpoint and epoch its stamp names, beside the credential it stamps), is
99
99
  * marked `torn` and is never a candidate; every other row is as on disk.
100
+ * With `requireCredentialStamps`, a row whose stamp is not `bound` is marked
101
+ * `unbound` and is never a candidate either. A torn row is shown with the
102
+ * replacement's stamp, which binds the completed row, so it is not unbound:
103
+ * it stays out of routing only until its completion is written.
100
104
  */
101
- export function loadRows(config, state, codec) {
105
+ export function loadRows(config, state, codec, options = {}) {
102
106
  const { config: whole, torn } = completeTornRows(config, state, codec);
103
107
  const rows = buildRawRows(whole, state, codec);
104
- if (torn.length === 0)
105
- return rows;
106
108
  for (const row of rows) {
107
- if (!torn.includes(row.id))
108
- continue;
109
- row.torn = true;
110
- row.candidate = false;
109
+ if (torn.includes(row.id)) {
110
+ row.torn = true;
111
+ row.candidate = false;
112
+ }
113
+ if (options.requireCredentialStamps && row.stamp !== 'bound') {
114
+ row.unbound = true;
115
+ row.candidate = false;
116
+ }
111
117
  }
112
118
  return rows;
113
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/common-auth",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Shared code for the CortexKit auth plugins: account pool, quota and routing, commands and auth menu, OpenCode 2 hooks, Claustrum custody, and plumbing (loopback RPC, file locks, logger, sidebar state, TUI preferences and build).",
5
5
  "license": "MIT",
6
6
  "repository": {