@cortexkit/common-auth 0.3.0 → 0.4.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.
Files changed (60) hide show
  1. package/dist/cachekeep/manager.d.ts +18 -6
  2. package/dist/cachekeep/manager.js +40 -10
  3. package/dist/claustrum/consumer.d.ts +13 -4
  4. package/dist/claustrum/consumer.js +11 -3
  5. package/dist/claustrum/custody.d.ts +47 -6
  6. package/dist/claustrum/custody.js +37 -7
  7. package/dist/claustrum/errors.d.ts +1 -1
  8. package/dist/claustrum/index.d.ts +3 -3
  9. package/dist/claustrum/index.js +2 -2
  10. package/dist/claustrum/interlock.d.ts +15 -17
  11. package/dist/claustrum/interlock.js +19 -26
  12. package/dist/claustrum/roster.d.ts +96 -6
  13. package/dist/claustrum/roster.js +237 -45
  14. package/dist/commands/builtins.d.ts +1 -1
  15. package/dist/commands/builtins.js +6 -1
  16. package/dist/commands/index.d.ts +2 -2
  17. package/dist/commands/index.js +1 -1
  18. package/dist/commands/menu.d.ts +8 -0
  19. package/dist/commands/menu.js +34 -13
  20. package/dist/commands/model.d.ts +9 -0
  21. package/dist/commands/seam.d.ts +40 -4
  22. package/dist/commands/seam.js +132 -19
  23. package/dist/dump/index.d.ts +94 -0
  24. package/dist/dump/index.js +236 -9
  25. package/dist/logger/engine.d.ts +52 -17
  26. package/dist/logger/engine.js +178 -135
  27. package/dist/logger/index.d.ts +2 -2
  28. package/dist/logger/index.js +1 -1
  29. package/dist/opencode2/install.d.ts +8 -3
  30. package/dist/opencode2/install.js +18 -9
  31. package/dist/opencode2/types.d.ts +22 -3
  32. package/dist/quota/projection.d.ts +11 -4
  33. package/dist/quota/projection.js +11 -4
  34. package/dist/routing/admission.js +3 -1
  35. package/dist/routing/index.d.ts +2 -2
  36. package/dist/routing/index.js +1 -1
  37. package/dist/routing/sticky.d.ts +19 -6
  38. package/dist/routing/sticky.js +34 -23
  39. package/dist/rpc/notifications.d.ts +20 -0
  40. package/dist/rpc/notifications.js +21 -0
  41. package/dist/rpc/rpc-server.d.ts +9 -1
  42. package/dist/rpc/rpc-server.js +8 -1
  43. package/dist/sidebar-file/index.d.ts +1 -1
  44. package/dist/sidebar-file/sidebar-file.d.ts +50 -2
  45. package/dist/sidebar-file/sidebar-file.js +92 -21
  46. package/dist/store/attribution.js +14 -2
  47. package/dist/store/errors.d.ts +6 -3
  48. package/dist/store/identity.d.ts +13 -4
  49. package/dist/store/index.d.ts +1 -1
  50. package/dist/store/mutate.d.ts +27 -4
  51. package/dist/store/mutate.js +43 -26
  52. package/dist/store/pool.d.ts +16 -3
  53. package/dist/store/pool.js +7 -2
  54. package/dist/store/rows.d.ts +20 -6
  55. package/dist/store/rows.js +141 -46
  56. package/dist/store/schema.d.ts +74 -5
  57. package/dist/store/schema.js +112 -10
  58. package/dist/store/torn.d.ts +29 -0
  59. package/dist/store/torn.js +113 -0
  60. package/package.json +1 -1
@@ -25,6 +25,14 @@ export type ApiKeyCredential = {
25
25
  authHeader?: 'authorization-bearer' | 'x-api-key';
26
26
  };
27
27
  export type PoolCredential = OAuthCredential | ApiKeyCredential;
28
+ /**
29
+ * What `rotate` takes. Rotation refreshes the secret of the endpoint a row
30
+ * already has, so an API key may leave out `baseURL` (and `authHeader`) to
31
+ * keep the row's; one it gives must equal the row's.
32
+ */
33
+ export type RotateCredential = OAuthCredential | (Omit<ApiKeyCredential, 'baseURL'> & {
34
+ baseURL?: string;
35
+ });
28
36
  /** A credential as stored: an OAuth credential also carries its refresh stamp. */
29
37
  export type StoredCredential = (OAuthCredential & {
30
38
  lastRefreshedAt?: number;
@@ -58,6 +66,41 @@ export interface PoolRow {
58
66
  candidate: boolean;
59
67
  /** Set when the roster row or the per-row entry failed validation. */
60
68
  invalid?: 'roster' | 'entry';
69
+ /**
70
+ * Set when a replace stopped between its two writes: the state file holds
71
+ * the new credential, stamped with the epoch and the identity or endpoint
72
+ * it belongs to, and the config still holds the replaced row. The row is
73
+ * shown as the replace leaves it once completed, is never a candidate, and
74
+ * the next store write on it writes the config to match.
75
+ */
76
+ torn?: true;
77
+ }
78
+ /**
79
+ * Key, inside a state-file account entry, of the stamp naming what the
80
+ * credential beside it belongs to. Older readers ignore it.
81
+ */
82
+ export declare const CREDENTIAL_STAMP_KEY = "commonAuthPool";
83
+ /**
84
+ * The config-side half of a replacement: the identity the new credential
85
+ * belongs to (absent: none is known) and, for an API key, its endpoint.
86
+ */
87
+ export interface CredentialBinding {
88
+ identity?: string;
89
+ baseURL?: string;
90
+ authHeader?: 'authorization-bearer' | 'x-api-key';
91
+ }
92
+ /**
93
+ * Written beside every credential the store puts in the state file. It names
94
+ * the credential epoch the credential belongs to and a digest of its secret,
95
+ * so a stamp left beside a credential another writer put there afterwards is
96
+ * recognisable and ignored. A replace also records the binding it gives the
97
+ * row, which is what lets a reader complete a replace that stopped after
98
+ * writing the credential.
99
+ */
100
+ export interface CredentialStamp {
101
+ credentialEpoch: number;
102
+ digest: string;
103
+ binding?: CredentialBinding;
61
104
  }
62
105
  export type ConfigClassification = {
63
106
  status: 'ready';
@@ -88,6 +131,21 @@ export declare function idProblem(id: unknown): string | undefined;
88
131
  /** Same URL rule the older readers apply to an API-key row's `baseURL`. */
89
132
  export declare function isValidBaseURL(value: unknown): boolean;
90
133
  export declare function fingerprintOf(credential: PoolCredential | StoredCredential): string;
134
+ /**
135
+ * The digest a credential stamp carries. It is kept apart from the
136
+ * fingerprint (a different input prefix) so the persisted value is never the
137
+ * dedupe key.
138
+ */
139
+ export declare function credentialDigest(credential: PoolCredential | StoredCredential): string;
140
+ export declare function stampFor(credential: PoolCredential | StoredCredential, credentialEpoch: number, binding?: CredentialBinding): CredentialStamp;
141
+ /**
142
+ * A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
143
+ * adding one may give back the same number, so a replace would not move the
144
+ * epoch and nothing could tell the old credential's work from the new one's.
145
+ */
146
+ export declare function isCredentialEpoch(value: unknown): value is number;
147
+ /** A well-formed stamp, or undefined for anything else (which is ignored). */
148
+ export declare function parseStamp(raw: unknown): CredentialStamp | undefined;
91
149
  /** A parsed file, or the reason it could not be parsed. */
92
150
  export type FileRead = {
93
151
  exists: false;
@@ -102,12 +160,21 @@ export declare function classifyConfig(read: FileRead): ConfigClassification;
102
160
  export declare function classifyState(read: FileRead): StateClassification;
103
161
  export declare function rosterOf(config: Record<string, unknown>): unknown[];
104
162
  export declare function entriesOf(config: Record<string, unknown>): Record<string, unknown>;
163
+ /** The first roster row with this id (the one the pool loads). */
164
+ export declare function rosterRowIn(config: Record<string, unknown>, id: string): Record<string, unknown> | undefined;
165
+ /** The per-row entries of a config, created (empty) when absent. */
166
+ export declare function ensureEntries(config: Record<string, unknown>): Record<string, unknown>;
167
+ export declare function entryIn(config: Record<string, unknown>, id: string): Record<string, unknown> | undefined;
168
+ /** Sets an entry as an own property, so an id such as `toString` is safe. */
169
+ export declare function setEntryIn(config: Record<string, unknown>, id: string, entry: Record<string, unknown>): void;
105
170
  /**
106
- * Builds the rows of a ready pool. A roster row the older readers would
107
- * reject, a duplicate id, or a malformed per-row entry makes that one row
108
- * invalid (never a candidate) and blocks nothing else.
171
+ * Builds the rows of a ready pool from the files exactly as they are, without
172
+ * looking at credential stamps (see `loadRows` for the rows every reader
173
+ * gets). A roster row the older readers would reject, a duplicate id, or a
174
+ * malformed per-row entry makes that one row invalid (never a candidate) and
175
+ * blocks nothing else.
109
176
  */
110
- export declare function buildRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): PoolRow[];
177
+ export declare function buildRawRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): PoolRow[];
111
178
  /** The row-lock key: recorded wire identity when known, else the local id. */
112
179
  export declare function rowLockKey(row: Pick<PoolRow, 'id' | 'identity'>): string;
113
180
  /**
@@ -130,4 +197,6 @@ export declare function rosterRowFor(input: {
130
197
  /** The state-file fields a credential occupies, in the older readers' names. */
131
198
  export declare function stateFieldsFor(credential: PoolCredential, lastRefreshedAt: number | undefined): Record<string, unknown>;
132
199
  export declare function storedCredential(credential: PoolCredential, lastRefreshedAt: number | undefined): StoredCredential;
133
- export declare function credentialProblem(credential: unknown): string | undefined;
200
+ export declare function credentialProblem(credential: unknown, options?: {
201
+ baseURLOptional?: boolean;
202
+ }): string | undefined;
@@ -13,6 +13,11 @@ export const LEGACY_STORE_VERSION = 1;
13
13
  * strictly above it counts as absent when they compare two copies of a token.
14
14
  */
15
15
  export const REFRESH_STAMP_TOLERANCE_MS = 5 * 60_000;
16
+ /**
17
+ * Key, inside a state-file account entry, of the stamp naming what the
18
+ * credential beside it belongs to. Older readers ignore it.
19
+ */
20
+ export const CREDENTIAL_STAMP_KEY = POOL_KEY;
16
21
  export function isRecord(value) {
17
22
  return value != null && typeof value === 'object' && !Array.isArray(value);
18
23
  }
@@ -46,11 +51,78 @@ export function isValidBaseURL(value) {
46
51
  return false;
47
52
  }
48
53
  }
49
- export function fingerprintOf(credential) {
50
- const secret = credential.type === 'oauth'
54
+ function secretOf(credential) {
55
+ return credential.type === 'oauth'
51
56
  ? `oauth\0${credential.refresh}`
52
57
  : `api\0${credential.apiKey}`;
53
- return createHash('sha256').update(secret).digest('hex');
58
+ }
59
+ export function fingerprintOf(credential) {
60
+ return createHash('sha256').update(secretOf(credential)).digest('hex');
61
+ }
62
+ /**
63
+ * The digest a credential stamp carries. It is kept apart from the
64
+ * fingerprint (a different input prefix) so the persisted value is never the
65
+ * dedupe key.
66
+ */
67
+ export function credentialDigest(credential) {
68
+ return createHash('sha256')
69
+ .update(`credential-stamp\0${secretOf(credential)}`)
70
+ .digest('hex');
71
+ }
72
+ export function stampFor(credential, credentialEpoch, binding) {
73
+ return {
74
+ credentialEpoch,
75
+ digest: credentialDigest(credential),
76
+ ...(binding ? { binding: { ...binding } } : {}),
77
+ };
78
+ }
79
+ /**
80
+ * A credential epoch is a positive safe integer. Above `MAX_SAFE_INTEGER`,
81
+ * adding one may give back the same number, so a replace would not move the
82
+ * epoch and nothing could tell the old credential's work from the new one's.
83
+ */
84
+ export function isCredentialEpoch(value) {
85
+ return Number.isSafeInteger(value) && value >= 1;
86
+ }
87
+ /** A well-formed stamp, or undefined for anything else (which is ignored). */
88
+ export function parseStamp(raw) {
89
+ if (!isRecord(raw))
90
+ return undefined;
91
+ const epoch = raw.credentialEpoch;
92
+ if (!isCredentialEpoch(epoch))
93
+ return undefined;
94
+ if (typeof raw.digest !== 'string')
95
+ return undefined;
96
+ if (!('binding' in raw))
97
+ return { credentialEpoch: epoch, digest: raw.digest };
98
+ const binding = raw.binding;
99
+ if (!isRecord(binding))
100
+ return undefined;
101
+ if ('identity' in binding &&
102
+ (typeof binding.identity !== 'string' || !binding.identity))
103
+ return undefined;
104
+ if ('baseURL' in binding && !isValidBaseURL(binding.baseURL))
105
+ return undefined;
106
+ if ('authHeader' in binding &&
107
+ binding.authHeader !== 'authorization-bearer' &&
108
+ binding.authHeader !== 'x-api-key')
109
+ return undefined;
110
+ return {
111
+ credentialEpoch: epoch,
112
+ digest: raw.digest,
113
+ binding: {
114
+ ...(typeof binding.identity === 'string'
115
+ ? { identity: binding.identity }
116
+ : {}),
117
+ ...(typeof binding.baseURL === 'string'
118
+ ? { baseURL: binding.baseURL.trim() }
119
+ : {}),
120
+ ...(binding.authHeader === 'authorization-bearer' ||
121
+ binding.authHeader === 'x-api-key'
122
+ ? { authHeader: binding.authHeader }
123
+ : {}),
124
+ },
125
+ };
54
126
  }
55
127
  export function classifyConfig(read) {
56
128
  if (!read.exists)
@@ -126,7 +198,7 @@ function parseEntry(raw, codec) {
126
198
  if (!isRecord(raw))
127
199
  return undefined;
128
200
  const epoch = raw.credentialEpoch;
129
- if (typeof epoch !== 'number' || !Number.isInteger(epoch) || epoch < 1)
201
+ if (!isCredentialEpoch(epoch))
130
202
  return undefined;
131
203
  if ('needsFirstReading' in raw && typeof raw.needsFirstReading !== 'boolean')
132
204
  return undefined;
@@ -176,12 +248,41 @@ function credentialFor(raw, stateEntry) {
176
248
  : {}),
177
249
  };
178
250
  }
251
+ /** The first roster row with this id (the one the pool loads). */
252
+ export function rosterRowIn(config, id) {
253
+ return rosterOf(config).find((raw) => isRecord(raw) && raw.id === id);
254
+ }
255
+ /** The per-row entries of a config, created (empty) when absent. */
256
+ export function ensureEntries(config) {
257
+ if (!isRecord(config[POOL_KEY]))
258
+ config[POOL_KEY] = {};
259
+ const pool = config[POOL_KEY];
260
+ if (!isRecord(pool[POOL_ROWS_KEY]))
261
+ pool[POOL_ROWS_KEY] = {};
262
+ return pool[POOL_ROWS_KEY];
263
+ }
264
+ export function entryIn(config, id) {
265
+ const entries = entriesOf(config);
266
+ const entry = Object.hasOwn(entries, id) ? entries[id] : undefined;
267
+ return isRecord(entry) ? entry : undefined;
268
+ }
269
+ /** Sets an entry as an own property, so an id such as `toString` is safe. */
270
+ export function setEntryIn(config, id, entry) {
271
+ Object.defineProperty(ensureEntries(config), id, {
272
+ value: entry,
273
+ enumerable: true,
274
+ writable: true,
275
+ configurable: true,
276
+ });
277
+ }
179
278
  /**
180
- * Builds the rows of a ready pool. A roster row the older readers would
181
- * reject, a duplicate id, or a malformed per-row entry makes that one row
182
- * invalid (never a candidate) and blocks nothing else.
279
+ * Builds the rows of a ready pool from the files exactly as they are, without
280
+ * looking at credential stamps (see `loadRows` for the rows every reader
281
+ * gets). A roster row the older readers would reject, a duplicate id, or a
282
+ * malformed per-row entry makes that one row invalid (never a candidate) and
283
+ * blocks nothing else.
183
284
  */
184
- export function buildRows(config, state, codec) {
285
+ export function buildRawRows(config, state, codec) {
185
286
  const entries = entriesOf(config);
186
287
  const stateAccounts = stateAccountsOf(state);
187
288
  const seen = new Set();
@@ -302,7 +403,7 @@ export function storedCredential(credential, lastRefreshedAt) {
302
403
  ...(lastRefreshedAt !== undefined ? { lastRefreshedAt } : {}),
303
404
  };
304
405
  }
305
- export function credentialProblem(credential) {
406
+ export function credentialProblem(credential, options = {}) {
306
407
  if (!isRecord(credential))
307
408
  return 'credential must be an object';
308
409
  if (credential.type === 'oauth') {
@@ -315,7 +416,8 @@ export function credentialProblem(credential) {
315
416
  return 'api credential needs an api key';
316
417
  if (credential.apiKey.trim() !== credential.apiKey)
317
418
  return 'api key must not carry surrounding whitespace';
318
- if (!isValidBaseURL(credential.baseURL))
419
+ const omitted = options.baseURLOptional && credential.baseURL === undefined;
420
+ if (!omitted && !isValidBaseURL(credential.baseURL))
319
421
  return 'api credential needs a valid baseURL';
320
422
  return undefined;
321
423
  }
@@ -0,0 +1,29 @@
1
+ import { type RowEditor } from './identity.js';
2
+ import { type CredentialBinding, type CredentialStamp, type PoolRow, type QuotaCodec } from './schema.js';
3
+ /** Stamps of rows torn between the two writes of a replace, by row id. */
4
+ export declare function tornStamps(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): Map<string, CredentialStamp & {
5
+ binding: CredentialBinding;
6
+ }>;
7
+ /**
8
+ * The config half of a replacement: the row's entry moves to the new epoch,
9
+ * loses its quota and needs a first reading; its roster row gets the new
10
+ * identity (or loses the old one) and, for an API key, the new endpoint.
11
+ * Disabling later rows that share the new identity is left to the caller
12
+ * (`disableIdentityDuplicates`), so several rows can be bound first.
13
+ */
14
+ export declare function bindReplacement(editor: RowEditor, id: string, credentialEpoch: number, binding: CredentialBinding): void;
15
+ /**
16
+ * The config with every torn row completed as its replace would have left
17
+ * it, and the ids completed. The config passed in is not modified; when
18
+ * nothing is torn it is returned as is.
19
+ */
20
+ export declare function completeTornRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): {
21
+ config: Record<string, unknown>;
22
+ torn: string[];
23
+ };
24
+ /**
25
+ * The rows every reader gets. A torn row is shown completed (the identity,
26
+ * endpoint and epoch its stamp names, beside the credential it stamps), is
27
+ * marked `torn` and is never a candidate; every other row is as on disk.
28
+ */
29
+ export declare function loadRows(config: Record<string, unknown>, state: Record<string, unknown>, codec: QuotaCodec): PoolRow[];
@@ -0,0 +1,113 @@
1
+ import { disableIdentityDuplicates } from './identity.js';
2
+ import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, entryIn, isRecord, parseStamp, rosterRowIn, setEntryIn, } from './schema.js';
3
+ /*
4
+ * A replace changes both files: the state file gets the new credential and
5
+ * the config gets the new epoch, identity or endpoint. It writes the state
6
+ * file first, stamping the credential with the epoch it belongs to and the
7
+ * binding (identity, endpoint) the config is about to get. A crash between
8
+ * the two writes therefore leaves a credential whose stamp is ahead of the
9
+ * config's epoch, and that is how every reader tells such a torn row from a
10
+ * whole one. The stamp holds everything the config write would have written,
11
+ * so a torn row is completed forward: readers are shown the completed row
12
+ * (never a candidate until it is on disk) and the next store write on it
13
+ * writes the config to match.
14
+ *
15
+ * A stamp counts only beside the credential it was written with (its digest
16
+ * matches): a writer that does not know about stamps may put another
17
+ * credential beside an old one, and that stamp then says nothing. A stamp at
18
+ * or behind the config's epoch is never torn: this store's own writes leave
19
+ * the state file ahead of the config, never behind it.
20
+ */
21
+ /** Stamps of rows torn between the two writes of a replace, by row id. */
22
+ export function tornStamps(config, state, codec) {
23
+ const accounts = isRecord(state.accounts) ? state.accounts : {};
24
+ const torn = new Map();
25
+ for (const row of buildRawRows(config, state, codec)) {
26
+ if (row.invalid || !row.credential)
27
+ continue;
28
+ const account = Object.hasOwn(accounts, row.id)
29
+ ? accounts[row.id]
30
+ : undefined;
31
+ if (!isRecord(account))
32
+ continue;
33
+ const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
34
+ if (!stamp?.binding)
35
+ continue;
36
+ if (stamp.digest !== credentialDigest(row.credential))
37
+ continue;
38
+ // A row without an entry is at epoch 1, as everywhere else.
39
+ if (stamp.credentialEpoch <= (row.credentialEpoch ?? 1))
40
+ continue;
41
+ torn.set(row.id, { ...stamp, binding: stamp.binding });
42
+ }
43
+ return torn;
44
+ }
45
+ /**
46
+ * The config half of a replacement: the row's entry moves to the new epoch,
47
+ * loses its quota and needs a first reading; its roster row gets the new
48
+ * identity (or loses the old one) and, for an API key, the new endpoint.
49
+ * Disabling later rows that share the new identity is left to the caller
50
+ * (`disableIdentityDuplicates`), so several rows can be bound first.
51
+ */
52
+ export function bindReplacement(editor, id, credentialEpoch, binding) {
53
+ const entry = editor.entry(id) ?? {};
54
+ const next = {
55
+ ...entry,
56
+ credentialEpoch,
57
+ needsFirstReading: true,
58
+ };
59
+ delete next.quota;
60
+ editor.setEntry(id, next);
61
+ const raw = editor.rosterRow(id);
62
+ if (!raw)
63
+ return;
64
+ if (binding.identity !== undefined)
65
+ raw.accountId = binding.identity;
66
+ else
67
+ delete raw.accountId;
68
+ if (binding.baseURL !== undefined)
69
+ raw.baseURL = binding.baseURL;
70
+ if (binding.authHeader !== undefined)
71
+ raw.authHeader = binding.authHeader;
72
+ }
73
+ /**
74
+ * The config with every torn row completed as its replace would have left
75
+ * it, and the ids completed. The config passed in is not modified; when
76
+ * nothing is torn it is returned as is.
77
+ */
78
+ export function completeTornRows(config, state, codec) {
79
+ const stamps = tornStamps(config, state, codec);
80
+ if (stamps.size === 0)
81
+ return { config, torn: [] };
82
+ const next = structuredClone(config);
83
+ const editor = {
84
+ rows: () => buildRawRows(next, state, codec),
85
+ rosterRow: (id) => rosterRowIn(next, id),
86
+ entry: (id) => entryIn(next, id),
87
+ setEntry: (id, entry) => setEntryIn(next, id, entry),
88
+ };
89
+ for (const [id, stamp] of stamps)
90
+ bindReplacement(editor, id, stamp.credentialEpoch, stamp.binding);
91
+ for (const stamp of stamps.values())
92
+ if (stamp.binding.identity !== undefined)
93
+ disableIdentityDuplicates(editor, stamp.binding.identity);
94
+ return { config: next, torn: [...stamps.keys()] };
95
+ }
96
+ /**
97
+ * The rows every reader gets. A torn row is shown completed (the identity,
98
+ * endpoint and epoch its stamp names, beside the credential it stamps), is
99
+ * marked `torn` and is never a candidate; every other row is as on disk.
100
+ */
101
+ export function loadRows(config, state, codec) {
102
+ const { config: whole, torn } = completeTornRows(config, state, codec);
103
+ const rows = buildRawRows(whole, state, codec);
104
+ if (torn.length === 0)
105
+ return rows;
106
+ for (const row of rows) {
107
+ if (!torn.includes(row.id))
108
+ continue;
109
+ row.torn = true;
110
+ row.candidate = false;
111
+ }
112
+ return rows;
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/common-auth",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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": {