@cortexkit/antigravity-auth-core 2.2.1 → 3.0.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.
- package/README.md +24 -2
- package/dist/abort-forwarding.d.ts +3 -0
- package/dist/abort-forwarding.d.ts.map +1 -0
- package/dist/abort-forwarding.js +19 -0
- package/dist/abort-forwarding.js.map +1 -0
- package/dist/account-identity.d.ts +52 -0
- package/dist/account-identity.d.ts.map +1 -0
- package/dist/account-identity.js +66 -0
- package/dist/account-identity.js.map +1 -0
- package/dist/account-manager.d.ts +228 -43
- package/dist/account-manager.d.ts.map +1 -1
- package/dist/account-manager.js +942 -903
- package/dist/account-manager.js.map +1 -1
- package/dist/account-migration.d.ts +309 -0
- package/dist/account-migration.d.ts.map +1 -0
- package/dist/account-migration.js +2282 -0
- package/dist/account-migration.js.map +1 -0
- package/dist/account-repository-codecs.d.ts +82 -0
- package/dist/account-repository-codecs.d.ts.map +1 -0
- package/dist/account-repository-codecs.js +609 -0
- package/dist/account-repository-codecs.js.map +1 -0
- package/dist/account-repository-types.d.ts +831 -0
- package/dist/account-repository-types.d.ts.map +1 -0
- package/dist/account-repository-types.js +137 -0
- package/dist/account-repository-types.js.map +1 -0
- package/dist/account-repository.d.ts +442 -0
- package/dist/account-repository.d.ts.map +1 -0
- package/dist/account-repository.js +2253 -0
- package/dist/account-repository.js.map +1 -0
- package/dist/account-selector.d.ts +470 -0
- package/dist/account-selector.d.ts.map +1 -0
- package/dist/account-selector.js +1383 -0
- package/dist/account-selector.js.map +1 -0
- package/dist/account-storage.d.ts.map +1 -1
- package/dist/account-storage.js +5 -0
- package/dist/account-storage.js.map +1 -1
- package/dist/agy-transport.d.ts.map +1 -1
- package/dist/agy-transport.js +21 -35
- package/dist/agy-transport.js.map +1 -1
- package/dist/antigravity-command-menu.d.ts +171 -0
- package/dist/antigravity-command-menu.d.ts.map +1 -0
- package/dist/antigravity-command-menu.js +523 -0
- package/dist/antigravity-command-menu.js.map +1 -0
- package/dist/atomic-write.d.ts +18 -1
- package/dist/atomic-write.d.ts.map +1 -1
- package/dist/atomic-write.js +15 -3
- package/dist/atomic-write.js.map +1 -1
- package/dist/common-auth-embedded/NOTICE.txt +21 -0
- package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
- package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
- package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
- package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
- package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
- package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
- package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
- package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
- package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
- package/dist/common-auth-embedded/auth-menu/index.js +12 -0
- package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
- package/dist/common-auth-embedded/auth-menu/login.js +83 -0
- package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
- package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
- package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
- package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
- package/dist/common-auth-embedded/auth-menu/select.js +159 -0
- package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
- package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
- package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
- package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
- package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
- package/dist/common-auth-embedded/claustrum/custody.js +354 -0
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
- package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
- package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
- package/dist/common-auth-embedded/claustrum/errors.js +8 -0
- package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
- package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
- package/dist/common-auth-embedded/claustrum/index.js +22 -0
- package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
- package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
- package/dist/common-auth-embedded/claustrum/roster.js +536 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
- package/dist/common-auth-embedded/commands/builtins.js +534 -0
- package/dist/common-auth-embedded/commands/index.d.ts +10 -0
- package/dist/common-auth-embedded/commands/index.js +5 -0
- package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
- package/dist/common-auth-embedded/commands/menu.js +300 -0
- package/dist/common-auth-embedded/commands/model.d.ts +238 -0
- package/dist/common-auth-embedded/commands/model.js +16 -0
- package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
- package/dist/common-auth-embedded/commands/pi.js +206 -0
- package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
- package/dist/common-auth-embedded/commands/seam.js +301 -0
- package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
- package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
- package/dist/common-auth-embedded/fs/index.d.ts +7 -0
- package/dist/common-auth-embedded/fs/index.js +6 -0
- package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
- package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
- package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
- package/dist/common-auth-embedded/fs/with-lock.js +48 -0
- package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
- package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
- package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
- package/dist/common-auth-embedded/logger/engine.js +246 -0
- package/dist/common-auth-embedded/logger/index.d.ts +6 -0
- package/dist/common-auth-embedded/logger/index.js +3 -0
- package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
- package/dist/common-auth-embedded/logger/redact.js +83 -0
- package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
- package/dist/common-auth-embedded/pi-slot/index.js +2 -0
- package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
- package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
- package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
- package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
- package/dist/common-auth-embedded/quota/codec.js +12 -0
- package/dist/common-auth-embedded/quota/format.d.ts +46 -0
- package/dist/common-auth-embedded/quota/format.js +138 -0
- package/dist/common-auth-embedded/quota/index.d.ts +10 -0
- package/dist/common-auth-embedded/quota/index.js +5 -0
- package/dist/common-auth-embedded/quota/map.d.ts +63 -0
- package/dist/common-auth-embedded/quota/map.js +105 -0
- package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
- package/dist/common-auth-embedded/quota/merge.js +162 -0
- package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
- package/dist/common-auth-embedded/quota/projection.js +134 -0
- package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
- package/dist/common-auth-embedded/routing/admission.js +142 -0
- package/dist/common-auth-embedded/routing/index.d.ts +8 -0
- package/dist/common-auth-embedded/routing/index.js +4 -0
- package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
- package/dist/common-auth-embedded/routing/ordered.js +58 -0
- package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
- package/dist/common-auth-embedded/routing/pins.js +34 -0
- package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
- package/dist/common-auth-embedded/routing/sticky.js +339 -0
- package/dist/common-auth-embedded/source-output.json +1090 -0
- package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
- package/dist/common-auth-embedded/store/attribution.js +60 -0
- package/dist/common-auth-embedded/store/errors.d.ts +55 -0
- package/dist/common-auth-embedded/store/errors.js +38 -0
- package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
- package/dist/common-auth-embedded/store/hooks.js +34 -0
- package/dist/common-auth-embedded/store/identity.d.ts +39 -0
- package/dist/common-auth-embedded/store/identity.js +79 -0
- package/dist/common-auth-embedded/store/index.d.ts +21 -0
- package/dist/common-auth-embedded/store/index.js +7 -0
- package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
- package/dist/common-auth-embedded/store/mutate.js +500 -0
- package/dist/common-auth-embedded/store/pool.d.ts +196 -0
- package/dist/common-auth-embedded/store/pool.js +120 -0
- package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
- package/dist/common-auth-embedded/store/provider-state.js +226 -0
- package/dist/common-auth-embedded/store/publication.d.ts +47 -0
- package/dist/common-auth-embedded/store/publication.js +304 -0
- package/dist/common-auth-embedded/store/pull.d.ts +36 -0
- package/dist/common-auth-embedded/store/pull.js +143 -0
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
- package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
- package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
- package/dist/common-auth-embedded/store/refresh.js +225 -0
- package/dist/common-auth-embedded/store/reserved.js +13 -0
- package/dist/common-auth-embedded/store/rows.d.ts +313 -0
- package/dist/common-auth-embedded/store/rows.js +1258 -0
- package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
- package/dist/common-auth-embedded/store/runtime.js +68 -0
- package/dist/common-auth-embedded/store/schema.d.ts +443 -0
- package/dist/common-auth-embedded/store/schema.js +754 -0
- package/dist/common-auth-embedded/store/settings.d.ts +63 -0
- package/dist/common-auth-embedded/store/settings.js +120 -0
- package/dist/common-auth-embedded/store/torn.d.ts +78 -0
- package/dist/common-auth-embedded/store/torn.js +294 -0
- package/dist/common-auth-embedded/util/any-signal.js +42 -0
- package/dist/common-auth-runtime.d.ts +73 -0
- package/dist/common-auth-runtime.d.ts.map +1 -0
- package/dist/common-auth-runtime.js +65 -0
- package/dist/common-auth-runtime.js.map +1 -0
- package/dist/credential-redaction.d.ts +13 -0
- package/dist/credential-redaction.d.ts.map +1 -0
- package/dist/credential-redaction.js +230 -0
- package/dist/credential-redaction.js.map +1 -0
- package/dist/fetch-timeout.d.ts +4 -2
- package/dist/fetch-timeout.d.ts.map +1 -1
- package/dist/fetch-timeout.js +121 -14
- package/dist/fetch-timeout.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +35 -10
- package/dist/quota-manager.js.map +1 -1
- package/dist/quota-presentation.d.ts +10 -0
- package/dist/quota-presentation.d.ts.map +1 -0
- package/dist/quota-presentation.js +60 -0
- package/dist/quota-presentation.js.map +1 -0
- package/dist/quota-types.d.ts +2 -0
- package/dist/quota-types.d.ts.map +1 -1
- package/dist/rotation.d.ts +8 -0
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +24 -0
- package/dist/rotation.js.map +1 -1
- package/dist/transform/claude.d.ts +7 -3
- package/dist/transform/claude.d.ts.map +1 -1
- package/dist/transform/claude.js +6 -2
- package/dist/transform/claude.js.map +1 -1
- package/dist/transform/types.d.ts +1 -1
- package/dist/transform/types.d.ts.map +1 -1
- package/dist/vault-account-source.d.ts +183 -0
- package/dist/vault-account-source.d.ts.map +1 -0
- package/dist/vault-account-source.js +296 -0
- package/dist/vault-account-source.js.map +1 -0
- package/dist/vault-enrollment-authority.d.ts +16 -0
- package/dist/vault-enrollment-authority.d.ts.map +1 -0
- package/dist/vault-enrollment-authority.js +19 -0
- package/dist/vault-enrollment-authority.js.map +1 -0
- package/dist/vault-family.d.ts +11 -0
- package/dist/vault-family.d.ts.map +1 -0
- package/dist/vault-family.js +24 -0
- package/dist/vault-family.js.map +1 -0
- package/dist/vault-provider-state.d.ts +162 -0
- package/dist/vault-provider-state.d.ts.map +1 -0
- package/dist/vault-provider-state.js +422 -0
- package/dist/vault-provider-state.js.map +1 -0
- package/package.json +39 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { acquireRefreshFileLock } from '../fs/refresh-file-lock.js';
|
|
2
|
+
import { LockContentionError } from '../fs/with-lock.js';
|
|
3
|
+
/**
|
|
4
|
+
* Store lock defaults: a 15 000 ms bounded wait retried every 50 ms plus
|
|
5
|
+
* jitter, and a renewed 10 000 ms lease.
|
|
6
|
+
*/
|
|
7
|
+
export const POOL_LOCK_DEFAULTS = Object.freeze({
|
|
8
|
+
ttlMs: 10_000,
|
|
9
|
+
timeoutMs: 15_000,
|
|
10
|
+
retryMs: 50,
|
|
11
|
+
renew: true,
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Takes one lock, retrying while a live holder has it until `timeoutMs` has
|
|
15
|
+
* passed on the real clock, then throws `LockContentionError`. The lease is
|
|
16
|
+
* asserted as owned once acquired, so a caller never proceeds on a lease that
|
|
17
|
+
* expired during the wait.
|
|
18
|
+
*/
|
|
19
|
+
export async function acquirePoolLock(spec, defaults, env) {
|
|
20
|
+
const emit = (type) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = env.onLockEvent?.({
|
|
23
|
+
type,
|
|
24
|
+
name: spec.name,
|
|
25
|
+
path: spec.path,
|
|
26
|
+
});
|
|
27
|
+
if (result &&
|
|
28
|
+
(typeof result === 'object' || typeof result === 'function') &&
|
|
29
|
+
'then' in result &&
|
|
30
|
+
typeof result.then === 'function') {
|
|
31
|
+
void Promise.resolve(result).catch(() => { });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Lock observers must not affect acquisition or release.
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const options = { ...defaults, ...definedOnly(spec) };
|
|
39
|
+
const started = performance.now();
|
|
40
|
+
for (;;) {
|
|
41
|
+
const lock = await acquireRefreshFileLock({
|
|
42
|
+
name: spec.name,
|
|
43
|
+
path: spec.path,
|
|
44
|
+
ttlMs: options.ttlMs,
|
|
45
|
+
now: env.now,
|
|
46
|
+
renew: options.renew,
|
|
47
|
+
onContended: () => emit('contended'),
|
|
48
|
+
...(options.renewIntervalMs !== undefined
|
|
49
|
+
? { renewIntervalMs: options.renewIntervalMs }
|
|
50
|
+
: {}),
|
|
51
|
+
...(env.onLockStep
|
|
52
|
+
? {
|
|
53
|
+
onStep: (step) => env.onLockStep?.({ name: spec.name, path: spec.path }, step),
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
56
|
+
});
|
|
57
|
+
if (lock) {
|
|
58
|
+
emit('acquired');
|
|
59
|
+
const held = {
|
|
60
|
+
name: spec.name,
|
|
61
|
+
path: spec.path,
|
|
62
|
+
assertOwned: () => lock.assertOwned(),
|
|
63
|
+
release: async () => {
|
|
64
|
+
await lock.release();
|
|
65
|
+
emit('released');
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
await held.assertOwned();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
await held.release();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return held;
|
|
76
|
+
}
|
|
77
|
+
const remaining = options.timeoutMs - (performance.now() - started);
|
|
78
|
+
if (remaining <= 0) {
|
|
79
|
+
throw new LockContentionError({
|
|
80
|
+
target: spec.path,
|
|
81
|
+
name: spec.name,
|
|
82
|
+
timeoutMs: options.timeoutMs,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const jitter = Math.floor(Math.random() * (options.retryMs + 1));
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(options.retryMs + jitter, remaining)));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function definedOnly(spec) {
|
|
90
|
+
const out = {};
|
|
91
|
+
if (spec.ttlMs !== undefined)
|
|
92
|
+
out.ttlMs = spec.ttlMs;
|
|
93
|
+
if (spec.timeoutMs !== undefined)
|
|
94
|
+
out.timeoutMs = spec.timeoutMs;
|
|
95
|
+
if (spec.retryMs !== undefined)
|
|
96
|
+
out.retryMs = spec.retryMs;
|
|
97
|
+
if (spec.renew !== undefined)
|
|
98
|
+
out.renew = spec.renew;
|
|
99
|
+
if (spec.renewIntervalMs !== undefined)
|
|
100
|
+
out.renewIntervalMs = spec.renewIntervalMs;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Locks taken in order and released in reverse. Every acquisition re-asserts
|
|
105
|
+
* the leases already held, because a wait is exactly when an earlier lease
|
|
106
|
+
* can expire unnoticed.
|
|
107
|
+
*/
|
|
108
|
+
export class LockStack {
|
|
109
|
+
defaults;
|
|
110
|
+
env;
|
|
111
|
+
held = [];
|
|
112
|
+
constructor(defaults, env) {
|
|
113
|
+
this.defaults = defaults;
|
|
114
|
+
this.env = env;
|
|
115
|
+
}
|
|
116
|
+
async acquire(spec) {
|
|
117
|
+
const lock = await acquirePoolLock(spec, this.defaults, this.env);
|
|
118
|
+
this.held.push(lock);
|
|
119
|
+
await this.assertAll();
|
|
120
|
+
return lock;
|
|
121
|
+
}
|
|
122
|
+
async assertAll() {
|
|
123
|
+
for (const lock of this.held)
|
|
124
|
+
await lock.assertOwned();
|
|
125
|
+
}
|
|
126
|
+
/** Releases every lock taken after `mark` (a length of `held`), newest first. */
|
|
127
|
+
async releaseTo(mark) {
|
|
128
|
+
while (this.held.length > mark) {
|
|
129
|
+
const lock = this.held.pop();
|
|
130
|
+
await lock?.release().catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async releaseAll() {
|
|
134
|
+
await this.releaseTo(0);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PoolLockSpec } from './refresh-lock.js';
|
|
2
|
+
import { type FailureHook } from './rows.js';
|
|
3
|
+
import { type StoreRuntime } from './runtime.js';
|
|
4
|
+
import { type OAuthCredential, type PoolRow, type StoredCredential } from './schema.js';
|
|
5
|
+
/** What the injected provider refresh function returns. */
|
|
6
|
+
export interface ProviderRefreshResult {
|
|
7
|
+
access: string;
|
|
8
|
+
refresh: string;
|
|
9
|
+
expires: number;
|
|
10
|
+
expiresIn?: number;
|
|
11
|
+
/** The account's wire identity, when the provider reports one. */
|
|
12
|
+
identity?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Provider state that changes with the new token (needs the store's
|
|
15
|
+
* provider-state codec). It is merged with the row's value on disk
|
|
16
|
+
* (`ProviderStateCodec.merge`) and written in the same state write as the
|
|
17
|
+
* rotated credential, under the same commit fence. Left out, the row keeps
|
|
18
|
+
* its value.
|
|
19
|
+
*/
|
|
20
|
+
providerState?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export type ProviderRefresh = (credential: OAuthCredential & {
|
|
23
|
+
lastRefreshedAt?: number;
|
|
24
|
+
}, row: PoolRow) => Promise<ProviderRefreshResult>;
|
|
25
|
+
export interface RefreshOptions {
|
|
26
|
+
/** The one provider-wide lock serialising every provider call. */
|
|
27
|
+
providerLock?: PoolLockSpec;
|
|
28
|
+
/** Taken after the provider-wide lock in this order, released in reverse. */
|
|
29
|
+
extraLocks?: readonly PoolLockSpec[];
|
|
30
|
+
/**
|
|
31
|
+
* Awaited once an ordinary rotation is persisted and the store locks released.
|
|
32
|
+
* Never called for `identity-contradicted`: its credential must not propagate
|
|
33
|
+
* as the row's recorded account.
|
|
34
|
+
*/
|
|
35
|
+
onPersisted?: (rowId: string, credential: StoredCredential) => void | Promise<void>;
|
|
36
|
+
onFailure?: FailureHook;
|
|
37
|
+
/**
|
|
38
|
+
* Awaited before any lock is taken, on the locked re-read, and at commit
|
|
39
|
+
* time under the store locks; a reason refuses the refresh there, leaving
|
|
40
|
+
* the stored credential untouched and discarding any rotated material.
|
|
41
|
+
*/
|
|
42
|
+
refuse?: (row: PoolRow) => string | undefined | Promise<string | undefined>;
|
|
43
|
+
}
|
|
44
|
+
export type RefreshOutcome = {
|
|
45
|
+
status: 'rotated';
|
|
46
|
+
rowId: string;
|
|
47
|
+
credential: StoredCredential;
|
|
48
|
+
identity?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The provider's successor credential is stored bound to expectedIdentity,
|
|
52
|
+
* not returnedIdentity. The row stays disabled until the adapter supplies
|
|
53
|
+
* an identity-validated replacement credential through replace.
|
|
54
|
+
*/
|
|
55
|
+
| {
|
|
56
|
+
status: 'identity-contradicted';
|
|
57
|
+
rowId: string;
|
|
58
|
+
expectedIdentity: string;
|
|
59
|
+
returnedIdentity: string;
|
|
60
|
+
credential: StoredCredential;
|
|
61
|
+
} | {
|
|
62
|
+
status: 'refused';
|
|
63
|
+
rowId: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Refreshes one OAuth row. Locks are taken in the fixed order row lock,
|
|
68
|
+
* provider-wide lock, extra locks, and the store locks only around the
|
|
69
|
+
* capture and the commit; ownership of every held lease is asserted after
|
|
70
|
+
* each wait, immediately before the provider call and before each write.
|
|
71
|
+
*/
|
|
72
|
+
export declare function refreshRow(rt: StoreRuntime, id: string, provider: ProviderRefresh, options?: RefreshOptions): Promise<RefreshOutcome>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { PoolOperationError } from './errors.js';
|
|
3
|
+
import { assertNotInsideHook, runInsideHook } from './hooks.js';
|
|
4
|
+
import { IDENTITY_CONTRADICTED_REASON_PREFIX, recordIdentityIn, } from './identity.js';
|
|
5
|
+
import { runOperation, withTransaction } from './mutate.js';
|
|
6
|
+
import { acceptProviderState, mergedProviderState } from './provider-state.js';
|
|
7
|
+
import { assertNotReserved } from './reserved.js';
|
|
8
|
+
import { rotateIn } from './rows.js';
|
|
9
|
+
import { readRow, refusal, requireBound, rowLockSpec, } from './runtime.js';
|
|
10
|
+
import { rotationStamp, rotationStampUntrusted, rowLockKey, } from './schema.js';
|
|
11
|
+
import { applyTransition } from './torn.js';
|
|
12
|
+
function requireRefreshable(id, row) {
|
|
13
|
+
assertNotReserved('refresh', id, row?.staged);
|
|
14
|
+
if (!row)
|
|
15
|
+
throw refusal('refresh', id, 'unknown-row', `no row ${id} in the pool`);
|
|
16
|
+
if (row.invalid)
|
|
17
|
+
throw refusal('refresh', id, 'invalid-row', `row ${id} is invalid`);
|
|
18
|
+
if (row.type !== 'oauth' || row.credential?.type !== 'oauth')
|
|
19
|
+
throw refusal('refresh', id, 'no-credential', `row ${id} holds no OAuth credential`);
|
|
20
|
+
if (!row.enabled)
|
|
21
|
+
throw refusal('refresh', id, 'row-disabled', `row ${id} is disabled`);
|
|
22
|
+
return row;
|
|
23
|
+
}
|
|
24
|
+
function stampAhead(id) {
|
|
25
|
+
return refusal('refresh', id, 'refresh-stamp-ahead', `row ${id}'s stored refresh stamp is ahead of the clock; a rotation now could not be stamped newer within the trust bound`, true);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Refreshes one OAuth row. Locks are taken in the fixed order row lock,
|
|
29
|
+
* provider-wide lock, extra locks, and the store locks only around the
|
|
30
|
+
* capture and the commit; ownership of every held lease is asserted after
|
|
31
|
+
* each wait, immediately before the provider call and before each write.
|
|
32
|
+
*/
|
|
33
|
+
export async function refreshRow(rt, id, provider, options = {}) {
|
|
34
|
+
assertNotInsideHook('refresh');
|
|
35
|
+
const { ctx } = rt;
|
|
36
|
+
return runOperation(ctx, 'refresh', id, options.onFailure, async (locks, progress) => {
|
|
37
|
+
const { row: seen } = await readRow(rt, 'refresh', id);
|
|
38
|
+
requireRefreshable(id, seen);
|
|
39
|
+
const early = await options.refuse?.(seen);
|
|
40
|
+
if (early !== undefined)
|
|
41
|
+
return { status: 'refused', rowId: id, reason: early };
|
|
42
|
+
let key = rowLockKey(seen);
|
|
43
|
+
let captured;
|
|
44
|
+
for (let attempt = 0;; attempt++) {
|
|
45
|
+
await locks.acquire(rowLockSpec(rt, { id, identity: key === id ? undefined : key }));
|
|
46
|
+
await locks.acquire(options.providerLock ?? rt.providerLock);
|
|
47
|
+
for (const extra of options.extraLocks ?? [])
|
|
48
|
+
await locks.acquire(extra);
|
|
49
|
+
// The capture may write the config once, to give a row its per-row
|
|
50
|
+
// entry. That write is setup, not the rotation, so it is counted apart
|
|
51
|
+
// and never makes a later failure report `after-first-write`.
|
|
52
|
+
const captureProgress = { writes: 0 };
|
|
53
|
+
const read = await withTransaction(ctx, locks, captureProgress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
54
|
+
let row = requireRefreshable(id, tx.row(id));
|
|
55
|
+
requireBound('refresh', row);
|
|
56
|
+
if (!row.hasEntry) {
|
|
57
|
+
tx.setEntry(id, { credentialEpoch: 1, needsFirstReading: true });
|
|
58
|
+
await tx.commitConfig();
|
|
59
|
+
row = requireRefreshable(id, tx.row(id));
|
|
60
|
+
}
|
|
61
|
+
if (rowLockKey(row) !== key)
|
|
62
|
+
return { keyNow: rowLockKey(row) };
|
|
63
|
+
const reason = await options.refuse?.(row);
|
|
64
|
+
if (reason !== undefined)
|
|
65
|
+
return { refused: reason };
|
|
66
|
+
return {
|
|
67
|
+
row,
|
|
68
|
+
credential: row.credential,
|
|
69
|
+
credentialEpoch: row.credentialEpoch,
|
|
70
|
+
identity: row.identity,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
if ('refused' in read)
|
|
74
|
+
return { status: 'refused', rowId: id, reason: read.refused };
|
|
75
|
+
if ('keyNow' in read) {
|
|
76
|
+
await locks.releaseAll();
|
|
77
|
+
if (attempt >= 1)
|
|
78
|
+
throw refusal('refresh', id, 'row-key-changed', `row ${id}'s wire identity changed twice while its lock was held`, true);
|
|
79
|
+
key = read.keyNow;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
captured = read;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (rotationStampUntrusted(captured.credential.lastRefreshedAt, ctx.now()))
|
|
86
|
+
throw stampAhead(id);
|
|
87
|
+
await ctx.hold?.('refresh-before-provider', id);
|
|
88
|
+
await locks.assertAll();
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = await provider(captured.credential, captured.row);
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
throw new PoolOperationError({
|
|
95
|
+
operation: 'refresh',
|
|
96
|
+
rowId: id,
|
|
97
|
+
phase: 'before-first-write',
|
|
98
|
+
retryable: true,
|
|
99
|
+
kind: 'provider',
|
|
100
|
+
message: 'the provider refresh failed',
|
|
101
|
+
cause,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (typeof result?.refresh !== 'string' || !result.refresh.trim())
|
|
105
|
+
throw refusal('refresh', id, 'provider', 'the provider returned no refresh token', true);
|
|
106
|
+
const incoming = result.providerState === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: acceptProviderState(ctx.providerState, 'refresh', id, result.providerState, 'the provider state the provider returned');
|
|
109
|
+
const commit = await withTransaction(ctx, locks, progress, { operation: 'refresh', rowId: id }, async (tx) => {
|
|
110
|
+
const current = tx.row(id);
|
|
111
|
+
const entry = tx.entry(id);
|
|
112
|
+
if (!current ||
|
|
113
|
+
current.invalid ||
|
|
114
|
+
!entry ||
|
|
115
|
+
entry.credentialEpoch !== captured.credentialEpoch ||
|
|
116
|
+
current.identity !== captured.identity)
|
|
117
|
+
throw new PoolOperationError({
|
|
118
|
+
operation: 'refresh',
|
|
119
|
+
rowId: id,
|
|
120
|
+
phase: 'before-first-write',
|
|
121
|
+
retryable: true,
|
|
122
|
+
kind: 'attribution',
|
|
123
|
+
message: `row ${id} changed credential while its refresh was in flight; the rotation is discarded`,
|
|
124
|
+
});
|
|
125
|
+
// The epoch fence above does not see a credential another writer
|
|
126
|
+
// swapped in under the same epoch; the stamp does, and committing
|
|
127
|
+
// the rotation would stamp the swapped row as bound.
|
|
128
|
+
requireBound('refresh', current);
|
|
129
|
+
const reason = await options.refuse?.(current);
|
|
130
|
+
if (reason !== undefined)
|
|
131
|
+
return { refused: reason };
|
|
132
|
+
const now = ctx.now();
|
|
133
|
+
const prior = current.credential?.type === 'oauth'
|
|
134
|
+
? current.credential.lastRefreshedAt
|
|
135
|
+
: undefined;
|
|
136
|
+
if (rotationStampUntrusted(prior, now))
|
|
137
|
+
throw stampAhead(id);
|
|
138
|
+
const credential = {
|
|
139
|
+
type: 'oauth',
|
|
140
|
+
access: result.access,
|
|
141
|
+
refresh: result.refresh,
|
|
142
|
+
expires: result.expires,
|
|
143
|
+
};
|
|
144
|
+
const contradiction = current.identity !== undefined &&
|
|
145
|
+
result.identity !== undefined &&
|
|
146
|
+
current.identity !== result.identity
|
|
147
|
+
? {
|
|
148
|
+
expectedIdentity: current.identity,
|
|
149
|
+
returnedIdentity: result.identity,
|
|
150
|
+
}
|
|
151
|
+
: undefined;
|
|
152
|
+
const transition = contradiction
|
|
153
|
+
? {
|
|
154
|
+
mark: randomUUID(),
|
|
155
|
+
enabled: false,
|
|
156
|
+
reason: `${IDENTITY_CONTRADICTED_REASON_PREFIX}${JSON.stringify(contradiction)}`,
|
|
157
|
+
}
|
|
158
|
+
: undefined;
|
|
159
|
+
const learnt = current.identity === undefined && result.identity
|
|
160
|
+
? result.identity
|
|
161
|
+
: undefined;
|
|
162
|
+
// The rotated credential's stamp names a learnt identity before the
|
|
163
|
+
// config records it, so a crash between the two writes is completed
|
|
164
|
+
// forward rather than leaving an identity no stamp proves.
|
|
165
|
+
const stored = await rotateIn(rt, tx, id, credential, {
|
|
166
|
+
stamp: rotationStamp(prior, now),
|
|
167
|
+
// Write state first with the disable marker, then config below.
|
|
168
|
+
// Once the successor is durable, recovery projects the disable even
|
|
169
|
+
// if config has not landed. A crash before the first rename still
|
|
170
|
+
// loses an in-memory provider reply, as in any ordinary refresh.
|
|
171
|
+
transition,
|
|
172
|
+
identity: learnt,
|
|
173
|
+
...(incoming !== undefined
|
|
174
|
+
? {
|
|
175
|
+
providerState: mergedProviderState(ctx.providerState, 'refresh', id, current.providerState, incoming),
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
});
|
|
179
|
+
if (transition !== undefined) {
|
|
180
|
+
applyTransition(tx, id, transition);
|
|
181
|
+
await tx.commitConfig();
|
|
182
|
+
}
|
|
183
|
+
let identity = current.identity;
|
|
184
|
+
if (learnt !== undefined) {
|
|
185
|
+
recordIdentityIn(tx, id, learnt);
|
|
186
|
+
identity = learnt;
|
|
187
|
+
await tx.commitConfig();
|
|
188
|
+
}
|
|
189
|
+
return { stored, identity, contradiction, refused: undefined };
|
|
190
|
+
});
|
|
191
|
+
if (commit.refused !== undefined)
|
|
192
|
+
return { status: 'refused', rowId: id, reason: commit.refused };
|
|
193
|
+
if (commit.contradiction !== undefined)
|
|
194
|
+
return {
|
|
195
|
+
status: 'identity-contradicted',
|
|
196
|
+
rowId: id,
|
|
197
|
+
...commit.contradiction,
|
|
198
|
+
credential: commit.stored,
|
|
199
|
+
};
|
|
200
|
+
if (options.onPersisted) {
|
|
201
|
+
try {
|
|
202
|
+
const persisted = options.onPersisted;
|
|
203
|
+
await runInsideHook('refresh', () => persisted(id, commit.stored));
|
|
204
|
+
}
|
|
205
|
+
catch (cause) {
|
|
206
|
+
throw new PoolOperationError({
|
|
207
|
+
operation: 'refresh',
|
|
208
|
+
rowId: id,
|
|
209
|
+
phase: 'after-first-write',
|
|
210
|
+
retryable: false,
|
|
211
|
+
kind: 'after-persist-hook',
|
|
212
|
+
committed: commit.stored,
|
|
213
|
+
message: 'the after-persist hook threw; the rotation stays committed',
|
|
214
|
+
cause,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
status: 'rotated',
|
|
220
|
+
rowId: id,
|
|
221
|
+
credential: commit.stored,
|
|
222
|
+
...(commit.identity !== undefined ? { identity: commit.identity } : {}),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PoolOperationError } from './errors.js';
|
|
2
|
+
/** A stored marker reserves the row even when the rest of its entry is invalid. */
|
|
3
|
+
export function assertNotReserved(operation, id, reservation) {
|
|
4
|
+
if (reservation !== undefined)
|
|
5
|
+
throw new PoolOperationError({
|
|
6
|
+
operation,
|
|
7
|
+
rowId: id,
|
|
8
|
+
kind: 'row-staged',
|
|
9
|
+
phase: operation === 'pull' ? 'pull' : 'before-first-write',
|
|
10
|
+
retryable: false,
|
|
11
|
+
message: `row ${id} is reserved for roster publication`,
|
|
12
|
+
});
|
|
13
|
+
}
|