@oxy.so/core 4.1.0 → 4.2.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/dist/cjs/HttpService.js +142 -33
- package/dist/cjs/api/accounts.js +49 -10
- package/dist/cjs/api/apps.js +2 -2
- package/dist/cjs/api/auth.js +24 -3
- package/dist/cjs/api/billing.js +35 -0
- package/dist/cjs/boot/sessionColdBoot.js +28 -57
- package/dist/cjs/inference/OxyInferenceClient.js +23 -15
- package/dist/cjs/server/OxyServer.js +107 -51
- package/dist/cjs/server/agentAccount.js +19 -0
- package/dist/cjs/server/auth.js +19 -0
- package/dist/cjs/server/capabilityTicket.js +3 -41
- package/dist/cjs/server/index.js +10 -2
- package/dist/cjs/server/liveCapabilityTicket.js +48 -0
- package/dist/cjs/server/middleware.js +60 -18
- package/dist/cjs/server/namespaces.js +57 -0
- package/dist/cjs/server/serviceTokenLifetime.js +13 -0
- package/dist/cjs/session/SessionClient.js +90 -12
- package/dist/cjs/session/authStateStore.js +130 -39
- package/dist/cjs/session/refresh.js +152 -30
- package/dist/cjs/session/sharedDeviceCredential.js +17 -1
- package/dist/cjs/utils/officialOrigins.js +7 -18
- package/dist/esm/HttpService.js +143 -34
- package/dist/esm/api/accounts.js +38 -0
- package/dist/esm/api/apps.js +2 -2
- package/dist/esm/api/auth.js +23 -3
- package/dist/esm/api/billing.js +35 -0
- package/dist/esm/boot/sessionColdBoot.js +29 -58
- package/dist/esm/inference/OxyInferenceClient.js +21 -13
- package/dist/esm/server/OxyServer.js +107 -51
- package/dist/esm/server/agentAccount.js +16 -0
- package/dist/esm/server/auth.js +18 -0
- package/dist/esm/server/capabilityTicket.js +1 -39
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/liveCapabilityTicket.js +45 -0
- package/dist/esm/server/middleware.js +60 -18
- package/dist/esm/server/namespaces.js +57 -0
- package/dist/esm/server/serviceTokenLifetime.js +9 -0
- package/dist/esm/session/SessionClient.js +90 -12
- package/dist/esm/session/authStateStore.js +128 -38
- package/dist/esm/session/refresh.js +151 -30
- package/dist/esm/session/sharedDeviceCredential.js +17 -1
- package/dist/esm/utils/officialOrigins.js +2 -13
- package/dist/types/HttpService.d.ts +26 -12
- package/dist/types/api/accounts.d.ts +38 -0
- package/dist/types/api/agency.d.ts +3 -1
- package/dist/types/api/apps.d.ts +3 -1
- package/dist/types/api/auth.d.ts +12 -1
- package/dist/types/api/billing.d.ts +12 -0
- package/dist/types/api/session.d.ts +3 -1
- package/dist/types/boot/sessionColdBoot.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +9 -0
- package/dist/types/inference/index.d.ts +1 -1
- package/dist/types/server/OxyServer.d.ts +16 -9
- package/dist/types/server/agentAccount.d.ts +14 -0
- package/dist/types/server/auth.d.ts +18 -0
- package/dist/types/server/capabilityTicket.d.ts +2 -4
- package/dist/types/server/index.d.ts +7 -1
- package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
- package/dist/types/server/middleware.d.ts +15 -6
- package/dist/types/server/namespaces.d.ts +78 -2
- package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
- package/dist/types/session/SessionClient.d.ts +8 -0
- package/dist/types/session/authStateStore.d.ts +15 -0
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +32 -7
- package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
- package/dist/types/utils/officialOrigins.d.ts +1 -5
- package/package.json +6 -6
- package/src/HttpService.ts +138 -50
- package/src/api/accounts.ts +22 -0
- package/src/api/agency.ts +3 -1
- package/src/api/apps.ts +2 -2
- package/src/api/auth.ts +31 -2
- package/src/api/billing.ts +71 -9
- package/src/api/session.ts +3 -1
- package/src/boot/sessionColdBoot.ts +27 -72
- package/src/index.ts +1 -1
- package/src/inference/OxyInferenceClient.ts +19 -5
- package/src/inference/index.ts +1 -0
- package/src/server/OxyServer.ts +114 -53
- package/src/server/agentAccount.ts +28 -0
- package/src/server/auth.ts +25 -0
- package/src/server/capabilityTicket.ts +2 -42
- package/src/server/index.ts +13 -0
- package/src/server/liveCapabilityTicket.ts +47 -0
- package/src/server/middleware.ts +72 -25
- package/src/server/namespaces.ts +99 -2
- package/src/server/serviceTokenLifetime.ts +13 -0
- package/src/session/SessionClient.ts +85 -12
- package/src/session/authStateStore.ts +130 -46
- package/src/session/index.ts +1 -0
- package/src/session/refresh.ts +144 -28
- package/src/session/sharedDeviceCredential.ts +14 -2
- package/src/utils/officialOrigins.ts +2 -13
package/dist/esm/api/accounts.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oxy.accounts` — the unified account graph (`/accounts`).
|
|
3
|
+
*
|
|
4
|
+
* An account is a relational, tree-structured principal (the `User` document
|
|
5
|
+
* generalised): a `personal` account is a human login at the root of its tree;
|
|
6
|
+
* `organization` / `project` / `bot` / `channel` accounts are non-login
|
|
7
|
+
* principals operated through membership. Accounts form a tree
|
|
8
|
+
* (`parentAccountId`) and expose a single membership model (`AccountMember`)
|
|
9
|
+
* with a unified role set and an explicit-but-inheritable cascade down the
|
|
10
|
+
* subtree. The applications an account owns are `oxy.apps`.
|
|
11
|
+
*
|
|
12
|
+
* Reference accounts by their `_id` (`accountId`, the underlying `User._id`)
|
|
13
|
+
* and members by their member `_id` — never by name, slug, or handle.
|
|
14
|
+
*
|
|
15
|
+
* SWITCHING INTO AN ACCOUNT: `accounts.actAs(accountId)` mints a REAL
|
|
16
|
+
* session for the target account and plants it as the active session — there is
|
|
17
|
+
* no per-request "acting-as" header. Identity is carried by the session/token,
|
|
18
|
+
* so a switch propagates through reload and cross-domain exactly like a login.
|
|
19
|
+
*/
|
|
20
|
+
import { agentKeyOperationSchema, executeAgentKeyOperationSchema, agentKeyOperationChallengeSchema, agentKeyOperationResultSchema, agentKeyListResponseSchema } from '@oxy.so/contracts';
|
|
1
21
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
2
22
|
import { evictOxyIdentityCache } from '../utils/identityCacheSweep.js';
|
|
3
23
|
import { evictOxyAccountForestCache, oxyAccountDetailCacheKey, OXY_ACCOUNT_PER_ACCOUNT_CACHE_PREFIX, } from '../utils/accountCacheSweep.js';
|
|
@@ -67,10 +87,28 @@ export class AccountMembersApi {
|
|
|
67
87
|
return result;
|
|
68
88
|
}
|
|
69
89
|
}
|
|
90
|
+
/** Current account governors and autonomous rotation use action-bound proofs. */
|
|
91
|
+
export class AccountAgentKeysApi {
|
|
92
|
+
constructor(ctx) {
|
|
93
|
+
this.ctx = ctx;
|
|
94
|
+
}
|
|
95
|
+
async list(accountId) {
|
|
96
|
+
return agentKeyListResponseSchema.parse(await this.ctx.request('GET', `/accounts/${enc(accountId)}/agent-keys`, undefined, { cache: false }));
|
|
97
|
+
}
|
|
98
|
+
async requestChallenge(accountId, operation) {
|
|
99
|
+
const response = await this.ctx.request('POST', `/accounts/${enc(accountId)}/agent-keys/challenge`, agentKeyOperationSchema.parse(operation), { cache: false });
|
|
100
|
+
return agentKeyOperationChallengeSchema.parse(response);
|
|
101
|
+
}
|
|
102
|
+
async execute(accountId, operation, proof) {
|
|
103
|
+
const response = await this.ctx.request('POST', `/accounts/${enc(accountId)}/agent-keys/execute`, executeAgentKeyOperationSchema.parse({ operation, proof }), { cache: false });
|
|
104
|
+
return agentKeyOperationResultSchema.parse(response);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
70
107
|
export class AccountsApi {
|
|
71
108
|
constructor(ctx) {
|
|
72
109
|
this.ctx = ctx;
|
|
73
110
|
this.members = new AccountMembersApi(ctx);
|
|
111
|
+
this.agentKeys = new AccountAgentKeysApi(ctx);
|
|
74
112
|
}
|
|
75
113
|
/**
|
|
76
114
|
* List the accounts the caller can access: their own personal (root)
|
package/dist/esm/api/apps.js
CHANGED
|
@@ -167,10 +167,10 @@ export class AppsApi {
|
|
|
167
167
|
* consent, authorize, and device-flow approval screens before any session
|
|
168
168
|
* exists.
|
|
169
169
|
*/
|
|
170
|
-
async getPublic(clientId) {
|
|
170
|
+
async getPublic(clientId, options = {}) {
|
|
171
171
|
const res = await this.ctx.request('GET', `/auth/oauth/client/${enc(clientId)}`, undefined,
|
|
172
172
|
// Public client metadata (pre-session consent UI) — skip the bearer preflight.
|
|
173
|
-
{ cache: true, cacheTTL: MEDIUM_TTL, skipAuth: true });
|
|
173
|
+
{ cache: options.cache ?? true, cacheTTL: MEDIUM_TTL, skipAuth: true });
|
|
174
174
|
return res.application;
|
|
175
175
|
}
|
|
176
176
|
/**
|
package/dist/esm/api/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emailSignInLinkResponseSchema, emailSignInPendingSchema, emailSignInStartResponseSchema, emailVerificationConfirmResponseSchema, emailVerificationStartResponseSchema, loginResultSchema, safeParseContract, secondFactorRequiredSchema, signInMethodsSchema, totpBackupCodesResponseSchema, totpEnrollResponseSchema, } from '@oxy.so/contracts';
|
|
1
|
+
import { agentProofClaimsSchema, agentChallengeRequestSchema, agentVerifyRequestSchema, emailSignInLinkResponseSchema, emailSignInPendingSchema, emailSignInStartResponseSchema, emailVerificationConfirmResponseSchema, emailVerificationStartResponseSchema, loginResultSchema, safeParseContract, secondFactorRequiredSchema, signInMethodsSchema, totpBackupCodesResponseSchema, totpEnrollResponseSchema, } from '@oxy.so/contracts';
|
|
2
2
|
import { OxyAuthenticationError } from '../OxyServices.errors.js';
|
|
3
3
|
import { logger } from '../logger/index.js';
|
|
4
4
|
import { normalizeUserIdentity } from '../utils/userIdentity.js';
|
|
@@ -264,7 +264,8 @@ export class AuthOAuthApi {
|
|
|
264
264
|
* out, so the app can run the OAuth flow again. It never degrades into a
|
|
265
265
|
* session that looks alive and cannot refresh.
|
|
266
266
|
*/
|
|
267
|
-
|
|
267
|
+
/** `plantTokens: false` defers bearer installation to a validated provider commit. */
|
|
268
|
+
async exchangeCode(params, options = {}) {
|
|
268
269
|
const form = new URLSearchParams({
|
|
269
270
|
grant_type: 'authorization_code',
|
|
270
271
|
code: params.code,
|
|
@@ -295,7 +296,9 @@ export class AuthOAuthApi {
|
|
|
295
296
|
}
|
|
296
297
|
const expiresInSec = typeof record.expires_in === 'number' ? record.expires_in : DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
297
298
|
const expiresAt = new Date(Date.now() + expiresInSec * 1000).toISOString();
|
|
298
|
-
|
|
299
|
+
// Provider completion validates the expected subject before committing.
|
|
300
|
+
// Direct callers retain the historical token-planting default.
|
|
301
|
+
if (accessToken && options.plantTokens !== false) {
|
|
299
302
|
this.ctx.oxy.session.setAccessToken(accessToken);
|
|
300
303
|
}
|
|
301
304
|
if (!deviceId || !deviceSecret) {
|
|
@@ -642,6 +645,22 @@ export class AuthCommonsApi {
|
|
|
642
645
|
{ cache: false, skipAuth: true });
|
|
643
646
|
}
|
|
644
647
|
}
|
|
648
|
+
/** Bot entry uses caller-owned keys and the normal account session lane. */
|
|
649
|
+
export class AuthAgentApi {
|
|
650
|
+
constructor(ctx) {
|
|
651
|
+
this.ctx = ctx;
|
|
652
|
+
}
|
|
653
|
+
async requestChallenge(publicKey) {
|
|
654
|
+
const body = agentChallengeRequestSchema.parse({ publicKey });
|
|
655
|
+
const response = await this.ctx.request('POST', '/auth/agent/challenge', body, { cache: false, skipAuth: true });
|
|
656
|
+
return agentProofClaimsSchema.parse(response);
|
|
657
|
+
}
|
|
658
|
+
async verify(publicKey, proof) {
|
|
659
|
+
const body = agentVerifyRequestSchema.parse({ publicKey, ...proof });
|
|
660
|
+
const response = await this.ctx.request('POST', '/auth/agent/verify', body, { cache: false, skipAuth: true });
|
|
661
|
+
return plantSession(this.ctx, response, 'auth/agent/verify');
|
|
662
|
+
}
|
|
663
|
+
}
|
|
645
664
|
export class AuthApi {
|
|
646
665
|
constructor(ctx) {
|
|
647
666
|
this.ctx = ctx;
|
|
@@ -650,6 +669,7 @@ export class AuthApi {
|
|
|
650
669
|
this.totp = new AuthTotpApi(ctx);
|
|
651
670
|
this.oauth = new AuthOAuthApi(ctx);
|
|
652
671
|
this.commons = new AuthCommonsApi(ctx);
|
|
672
|
+
this.agent = new AuthAgentApi(ctx);
|
|
653
673
|
}
|
|
654
674
|
/**
|
|
655
675
|
* Register a new identity with public key authentication
|
package/dist/esm/api/billing.js
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oxy.billing` — the signed-in user's subscription, wallet and payment history.
|
|
3
|
+
*
|
|
4
|
+
* Every call reads the SIGNED-IN user's data unless a `userId` is passed (the
|
|
5
|
+
* API refuses any other user's). Checkout, plan changes and cancellation live
|
|
6
|
+
* under `/billing` (Stripe) and are not wrapped here yet.
|
|
7
|
+
*
|
|
8
|
+
* The types model the JSON these routes actually return (`subscription.controller`,
|
|
9
|
+
* `payment.controller`, `wallet.routes` in `packages/api`); `Date` columns
|
|
10
|
+
* arrive as ISO-8601 strings.
|
|
11
|
+
*/
|
|
12
|
+
import { subjectProductAccessQuerySchema, subjectProductAccessSchema, } from '@oxy.so/contracts';
|
|
13
|
+
import { productSubscriptionCancellationResultSchema, cancelProductSubscriptionSchema, productSubscriptionsResponseSchema, subscriptionCreditGrantsResponseSchema, } from '@oxy.so/contracts';
|
|
1
14
|
const SUBSCRIPTION_TTL = 2 * 60 * 1000;
|
|
2
15
|
const WALLET_TTL = 60 * 1000;
|
|
3
16
|
export class BillingApi {
|
|
4
17
|
constructor(ctx) {
|
|
5
18
|
this.ctx = ctx;
|
|
6
19
|
}
|
|
20
|
+
/** Product rights only; requires a production application-bound user session. Never cached. */
|
|
21
|
+
async productAccess(query) {
|
|
22
|
+
const parsed = subjectProductAccessQuerySchema.parse(query);
|
|
23
|
+
const response = await this.ctx.request('GET', `/v1/products/${encodeURIComponent(parsed.productId)}/access/${encodeURIComponent(parsed.subjectAccountId)}`, undefined, { cache: false });
|
|
24
|
+
return subjectProductAccessSchema.parse(response);
|
|
25
|
+
}
|
|
26
|
+
/** Named commercial sources for the current subject; no inferred plan collapse. */
|
|
27
|
+
async productSubscriptions() {
|
|
28
|
+
return productSubscriptionsResponseSchema.parse(await this.ctx.request("GET", "/billing/product-subscriptions", undefined, { cache: false })).subscriptions;
|
|
29
|
+
}
|
|
30
|
+
/** Per-grant credit provenance; no interpretation of the historical mixed balance. */
|
|
31
|
+
async creditGrants() {
|
|
32
|
+
return subscriptionCreditGrantsResponseSchema.parse(await this.ctx.request("GET", "/billing/credit-grants", undefined, { cache: false })).grants;
|
|
33
|
+
}
|
|
34
|
+
/** Payer-only named-source cancellation. Resolves on provider acceptance, including pending local reconciliation. Use cancelProductSubscriptionWithStatus for detailed completion state. */
|
|
35
|
+
async cancelProductSubscription(sourceId, expectedSubjectAccountId) {
|
|
36
|
+
await this.ctx.request("POST", "/billing/product-subscriptions/cancel", cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false });
|
|
37
|
+
}
|
|
38
|
+
/** Provider acceptance versus completed local cancellation, for user-facing flows. */
|
|
39
|
+
async cancelProductSubscriptionWithStatus(sourceId, expectedSubjectAccountId) {
|
|
40
|
+
return productSubscriptionCancellationResultSchema.parse(await this.ctx.request('POST', '/billing/product-subscriptions/cancel', cancelProductSubscriptionSchema.parse({ sourceId, ...(expectedSubjectAccountId ? { expectedSubjectAccountId } : {}) }), { cache: false }));
|
|
41
|
+
}
|
|
7
42
|
/** The signed-in user's payment history. Never cached. */
|
|
8
43
|
async payments() {
|
|
9
44
|
return this.ctx.request('GET', '/payments/user', undefined, { cache: false });
|
|
@@ -44,9 +44,8 @@ import { runColdBoot } from '../utils/coldBoot.js';
|
|
|
44
44
|
import { isNative as detectNative } from '../utils/platform.js';
|
|
45
45
|
import { logger } from '../logger/index.js';
|
|
46
46
|
import { computeIdentityTag } from '../utils/cacheKey.js';
|
|
47
|
-
import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm } from '../session/refresh.js';
|
|
47
|
+
import { TOKEN_REFRESH_LEAD_MS, refreshDeviceSecretArm, refreshSharedDeviceArm } from '../session/refresh.js';
|
|
48
48
|
import { establishIdentitySession, resolveIdentityPin, } from '../session/identitySession.js';
|
|
49
|
-
import { decideSharedDeviceJoin, } from '../session/sharedDeviceCredential.js';
|
|
50
49
|
/**
|
|
51
50
|
* Run the device-first cold boot. Resolves to the `runColdBoot` outcome and, as
|
|
52
51
|
* a side effect, invokes `onSession` (winning session, token already planted) or
|
|
@@ -70,6 +69,8 @@ export async function runSessionColdBoot(opts) {
|
|
|
70
69
|
// Boot-local (not module-level) so it cannot leak across boots or break under
|
|
71
70
|
// bundler re-evaluation.
|
|
72
71
|
let signedOutReason = 'no_session';
|
|
72
|
+
let rejectedLocalHolder = false;
|
|
73
|
+
let accountRecoverySuperseded = false;
|
|
73
74
|
// Boot-local memo for the identity pin. Resolved lazily INSIDE a step so the
|
|
74
75
|
// (local, but storage-backed) read is covered by `overallDeadlineMs`, and
|
|
75
76
|
// resolved at most once per boot so two steps cannot disagree. Always `null`
|
|
@@ -101,6 +102,8 @@ export async function runSessionColdBoot(opts) {
|
|
|
101
102
|
steps.push({
|
|
102
103
|
id: 'warm-token-plant',
|
|
103
104
|
run: async () => {
|
|
105
|
+
if (identityBinding === null && isNative && await store.isAutomaticIdentitySignInSuppressed?.())
|
|
106
|
+
return { kind: 'skip' };
|
|
104
107
|
const persisted = await store.load();
|
|
105
108
|
if (!persisted?.accessToken || !persisted.sessionId || !persisted.userId || !persisted.expiresAt) {
|
|
106
109
|
return { kind: 'skip' };
|
|
@@ -173,6 +176,11 @@ export async function runSessionColdBoot(opts) {
|
|
|
173
176
|
},
|
|
174
177
|
};
|
|
175
178
|
case 'invalid-secret': {
|
|
179
|
+
if (identityBinding === null && store.setAutomaticIdentitySignInSuppressed && await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
180
|
+
rejectedLocalHolder = true;
|
|
181
|
+
signedOutReason = 'no_session';
|
|
182
|
+
return { kind: 'skip' };
|
|
183
|
+
}
|
|
176
184
|
// Stale/diverged secret — drop it so the mint lane stops firing. On
|
|
177
185
|
// native the Commons-proof step below can still recover; on web this ends
|
|
178
186
|
// signed out. Setting it undefined drops the key on the store's JSON
|
|
@@ -184,6 +192,7 @@ export async function runSessionColdBoot(opts) {
|
|
|
184
192
|
return { kind: 'skip' };
|
|
185
193
|
}
|
|
186
194
|
case 'no-session':
|
|
195
|
+
rejectedLocalHolder = identityBinding === null;
|
|
187
196
|
// Device known, no live session — authoritative signed-out. Keep the
|
|
188
197
|
// secret (the device may sign in again).
|
|
189
198
|
signedOutReason = 'no_session';
|
|
@@ -205,7 +214,10 @@ export async function runSessionColdBoot(opts) {
|
|
|
205
214
|
logger.debug('device-secret mint failed (transient) — keeping secret', { component: 'sessionColdBoot', method: 'device-secret-mint' });
|
|
206
215
|
return { kind: 'skip' };
|
|
207
216
|
case 'session-ended':
|
|
208
|
-
//
|
|
217
|
+
// The prior holder/epoch was superseded. This boot must not turn a
|
|
218
|
+
// late rejection into a fresh Commons sign-in; a new boot can reconcile.
|
|
219
|
+
if (identityBinding === null)
|
|
220
|
+
accountRecoverySuperseded = true;
|
|
209
221
|
return { kind: 'skip' };
|
|
210
222
|
case 'no-secret':
|
|
211
223
|
return { kind: 'skip' };
|
|
@@ -270,63 +282,16 @@ export async function runSessionColdBoot(opts) {
|
|
|
270
282
|
// The adoption itself is local, but it is only worth committing alongside
|
|
271
283
|
// a mint that proves the credential — so the whole lane is online-gated
|
|
272
284
|
// like every other network step.
|
|
273
|
-
enabled: () => isNative && !isOffline(),
|
|
285
|
+
enabled: () => isNative && !isOffline() && !accountRecoverySuperseded,
|
|
274
286
|
run: async () => {
|
|
275
|
-
const
|
|
276
|
-
const decision = decideSharedDeviceJoin(before, await sharedSlot.read());
|
|
277
|
-
if (decision.action === 'skip') {
|
|
278
|
-
logger.debug(`shared device credential not adopted (${decision.reason})`, { component: 'sessionColdBoot', method: 'shared-device-adopt' });
|
|
279
|
-
return { kind: 'skip' };
|
|
280
|
-
}
|
|
281
|
-
// Restore the store to exactly what it held before this lane touched it.
|
|
282
|
-
// A credential we adopted and could not prove must not be left behind for
|
|
283
|
-
// the next boot's mint lane to keep retrying.
|
|
284
|
-
const revert = async () => {
|
|
285
|
-
if (before) {
|
|
286
|
-
await store.save(before);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
await store.clear();
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
const adopted = {
|
|
293
|
-
// The mint fills both in from the device's live state; carrying the
|
|
294
|
-
// previous session's ids into a different device session would be a
|
|
295
|
-
// lie for however long the mint takes.
|
|
296
|
-
sessionId: '',
|
|
297
|
-
userId: '',
|
|
298
|
-
deviceId: decision.credential.deviceId,
|
|
299
|
-
deviceSecret: decision.credential.deviceSecret,
|
|
300
|
-
};
|
|
301
|
-
if (!(await store.save(adopted))) {
|
|
302
|
-
logger.error('adopted the shared device credential but it could not be durably persisted — reverting', undefined, { component: 'sessionColdBoot', method: 'shared-device-adopt' });
|
|
303
|
-
await revert();
|
|
304
|
-
return { kind: 'skip' };
|
|
305
|
-
}
|
|
306
|
-
const result = await refreshDeviceSecretArm({ oxy, store, pin: null });
|
|
287
|
+
const result = await refreshSharedDeviceArm({ oxy, store, shared: sharedSlot, rejectedLocalHolder });
|
|
307
288
|
if (result.status === 'ok') {
|
|
308
|
-
return {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
sessionId: result.sessionId,
|
|
312
|
-
userId: result.userId,
|
|
313
|
-
accessToken: result.token,
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
if (result.status === 'invalid-secret') {
|
|
318
|
-
// The one place we hold POSITIVE proof that the exact bytes in the
|
|
319
|
-
// shared slot are dead — the server rejected them by name. Clearing it
|
|
320
|
-
// signs nobody out (a credential the server does not recognise cannot
|
|
321
|
-
// be minting for anyone) and it is what stops a dead credential from
|
|
322
|
-
// blocking every future install: a stale slot owned by a different
|
|
323
|
-
// `deviceId` is otherwise never overwritten, by design.
|
|
324
|
-
await sharedSlot.clear();
|
|
289
|
+
return { kind: 'session', session: {
|
|
290
|
+
sessionId: result.sessionId, userId: result.userId, accessToken: result.token, state: result.state,
|
|
291
|
+
} };
|
|
325
292
|
}
|
|
326
|
-
|
|
293
|
+
if (result.status === 'no-session')
|
|
327
294
|
signedOutReason = 'no_session';
|
|
328
|
-
}
|
|
329
|
-
await revert();
|
|
330
295
|
return { kind: 'skip' };
|
|
331
296
|
},
|
|
332
297
|
});
|
|
@@ -354,8 +319,11 @@ export async function runSessionColdBoot(opts) {
|
|
|
354
319
|
id: 'commons-proof-signin',
|
|
355
320
|
enabled: () => isNative && !isOffline(),
|
|
356
321
|
run: async () => {
|
|
357
|
-
|
|
358
|
-
|
|
322
|
+
if (accountRecoverySuperseded || await store.isAutomaticIdentitySignInSuppressed?.())
|
|
323
|
+
return { kind: 'skip' };
|
|
324
|
+
const epoch = oxy.http.getSessionEpoch();
|
|
325
|
+
const session = await oxy.auth.signInWithCommonsIdentity({ plantTokens: false, requestOptions: { retry: false } });
|
|
326
|
+
if (!session?.accessToken || oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.()) {
|
|
359
327
|
return { kind: 'skip' };
|
|
360
328
|
}
|
|
361
329
|
// `verifyChallenge` issues a deviceSecret; persist it so the next
|
|
@@ -371,6 +339,9 @@ export async function runSessionColdBoot(opts) {
|
|
|
371
339
|
expiresAt: session.expiresAt,
|
|
372
340
|
});
|
|
373
341
|
}
|
|
342
|
+
if (oxy.http.getSessionEpoch() !== epoch || await store.isAutomaticIdentitySignInSuppressed?.())
|
|
343
|
+
return { kind: 'skip' };
|
|
344
|
+
oxy.session.setAccessToken(session.accessToken);
|
|
374
345
|
return {
|
|
375
346
|
kind: 'session',
|
|
376
347
|
session: {
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_headers, _OxyInferenceClient_request;
|
|
1
13
|
/**
|
|
2
14
|
* The Oxy inference client — one surface, two credential lanes (issue #972,
|
|
3
15
|
* workstream 15).
|
|
@@ -49,18 +61,6 @@
|
|
|
49
61
|
* schema, so a rename on either side fails a build rather than a customer's
|
|
50
62
|
* request.
|
|
51
63
|
*/
|
|
52
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
53
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
54
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
55
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
56
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
57
|
-
};
|
|
58
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
59
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
60
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
61
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
62
|
-
};
|
|
63
|
-
var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_headers, _OxyInferenceClient_request;
|
|
64
64
|
import { decisionRequestSchema, decisionSuccessSchema, decisionAnswersMatch } from '@oxy.so/contracts';
|
|
65
65
|
import { INFERENCE_ERROR_CODES, MAX_INFERENCE_AUDIO_BYTES, inferenceAudioMediaTypeSchema, inferenceStreamEventSchema, modelIdSchema, } from '@oxy.so/contracts';
|
|
66
66
|
/** The base URL of the Oxy API, when a caller names none. */
|
|
@@ -379,7 +379,15 @@ export class OxyInferenceClient {
|
|
|
379
379
|
* somebody else.
|
|
380
380
|
*/
|
|
381
381
|
async getGeneration(id, options = {}) {
|
|
382
|
-
const
|
|
382
|
+
const record = await this.getGenerationRecord(id, options);
|
|
383
|
+
if (record.schemaVersion !== 1) {
|
|
384
|
+
throw new OxyInferenceProtocolError('Internal usage has no financial receipt. Use getGenerationRecord().', record.requestId);
|
|
385
|
+
}
|
|
386
|
+
return record;
|
|
387
|
+
}
|
|
388
|
+
/** Read a financial receipt (v1) or an internal technical record (v2). */
|
|
389
|
+
async getGenerationRecord(id, options = {}) {
|
|
390
|
+
const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, options);
|
|
383
391
|
return body.data;
|
|
384
392
|
}
|
|
385
393
|
}
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
* Node only.
|
|
27
27
|
*/
|
|
28
28
|
import { jwtDecode } from 'jwt-decode';
|
|
29
|
+
import { hasBoundedServiceTokenLifetime } from './serviceTokenLifetime.js';
|
|
30
|
+
/** Unverified cache metadata only: an old lifetime forces a new signed mint. */
|
|
31
|
+
function reusableServiceToken(token) {
|
|
32
|
+
try {
|
|
33
|
+
const claims = jwtDecode(token);
|
|
34
|
+
return hasBoundedServiceTokenLifetime(claims) && typeof claims.exp === 'number'
|
|
35
|
+
&& claims.exp > Math.floor(Date.now() / 1000);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
import { loadNodeCrypto } from '@oxy.so/protocol';
|
|
30
42
|
import { OxyServices } from '../OxyServices.js';
|
|
31
43
|
import { logger } from '../logger/index.js';
|
|
@@ -53,10 +65,10 @@ const WORKLOAD_CACHE_KEY = 'workload-identity';
|
|
|
53
65
|
const TOKEN_REUSE_MARGIN_MS = 60000;
|
|
54
66
|
/** Bound on remembered `appId:userId` delegation answers. */
|
|
55
67
|
const ACTING_AS_CACHE_MAX = 1000;
|
|
56
|
-
/**
|
|
57
|
-
const ACTING_AS_GRANT_TTL_MS =
|
|
58
|
-
/**
|
|
59
|
-
const ACTING_AS_DENIAL_TTL_MS =
|
|
68
|
+
/** Explicit read reuse only; effects always perform a fresh lookup. */
|
|
69
|
+
const ACTING_AS_GRANT_TTL_MS = 60000;
|
|
70
|
+
/** Explicit authoritative read denials last ten seconds; failures are never cached. */
|
|
71
|
+
const ACTING_AS_DENIAL_TTL_MS = 10000;
|
|
60
72
|
export class OxyServer extends OxyServices {
|
|
61
73
|
constructor(config) {
|
|
62
74
|
super(config);
|
|
@@ -65,6 +77,7 @@ export class OxyServer extends OxyServices {
|
|
|
65
77
|
this.serviceApiSecret = null;
|
|
66
78
|
this.actingAs = new Map();
|
|
67
79
|
this.actingAsPending = new Map();
|
|
80
|
+
this.actingAsGenerations = new Map();
|
|
68
81
|
/** Public keys only; never private material. */
|
|
69
82
|
this.jwksCache = { keys: new Map(), expiresAt: 0, lastAttemptAt: 0 };
|
|
70
83
|
/**
|
|
@@ -118,7 +131,7 @@ export class OxyServer extends OxyServices {
|
|
|
118
131
|
},
|
|
119
132
|
jwksCache: this.jwksCache,
|
|
120
133
|
validateSession: (sessionId, options) => this.session.validate(sessionId, options),
|
|
121
|
-
verifyActingAs: (appId, userId) => this.verifyActingAs(appId, userId),
|
|
134
|
+
verifyActingAs: (appId, userId, options) => this.verifyActingAs(appId, userId, options),
|
|
122
135
|
});
|
|
123
136
|
}
|
|
124
137
|
/** Files, plus the service-only metadata and linked-URL lookups. */
|
|
@@ -204,7 +217,7 @@ export class OxyServer extends OxyServices {
|
|
|
204
217
|
});
|
|
205
218
|
throw new ServiceCredentialMismatchError();
|
|
206
219
|
}
|
|
207
|
-
if (entry.token && entry.expiresAt > now + TOKEN_REUSE_MARGIN_MS) {
|
|
220
|
+
if (entry.token && reusableServiceToken(entry.token) && entry.expiresAt > now + TOKEN_REUSE_MARGIN_MS) {
|
|
208
221
|
return entry.token;
|
|
209
222
|
}
|
|
210
223
|
if (entry.pending) {
|
|
@@ -266,32 +279,49 @@ export class OxyServer extends OxyServices {
|
|
|
266
279
|
}
|
|
267
280
|
// ── Delegation and account events ────────────────────────────────────────
|
|
268
281
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* revocation window; a denial or failed lookup for 1 minute), bounded to the
|
|
275
|
-
* 1000 most recent pairs, and concurrent lookups of one pair share a single
|
|
276
|
-
* request.
|
|
282
|
+
* Resolve authority live by default. Explicit read-only reuse lasts at most
|
|
283
|
+
* 60s positive / 10s negative, anchored to request start; errors never cache.
|
|
284
|
+
* Effects do not share an older in-flight read. Durable epoch plus local
|
|
285
|
+
* completion ordering prevents ABA and late positive replies overwriting a
|
|
286
|
+
* newer refusal even when app/credential revocation leaves epoch unchanged.
|
|
277
287
|
*/
|
|
278
|
-
async verifyActingAs(appId, userId) {
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
async verifyActingAs(appId, userId, options = {}) {
|
|
289
|
+
const pair = JSON.stringify([appId, userId]);
|
|
290
|
+
const cacheKey = JSON.stringify([appId, userId, options.credentialId, options.ownerAccountId, options.environment]);
|
|
291
|
+
if (options.cache === true) {
|
|
292
|
+
const cached = this.actingAs.get(cacheKey);
|
|
293
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
294
|
+
this.actingAs.delete(cacheKey);
|
|
295
|
+
this.actingAs.set(cacheKey, cached);
|
|
296
|
+
return cached.result;
|
|
297
|
+
}
|
|
298
|
+
const pending = this.actingAsPending.get(cacheKey);
|
|
299
|
+
if (pending)
|
|
300
|
+
return pending;
|
|
301
|
+
}
|
|
302
|
+
let generation = this.actingAsGenerations.get(pair);
|
|
303
|
+
if (!generation) {
|
|
304
|
+
generation = { epoch: 0n, issued: 0, completed: 0 };
|
|
305
|
+
this.actingAsGenerations.set(pair, generation);
|
|
306
|
+
if (this.actingAsGenerations.size > ACTING_AS_CACHE_MAX) {
|
|
307
|
+
const oldest = this.actingAsGenerations.keys().next().value;
|
|
308
|
+
if (oldest !== undefined)
|
|
309
|
+
this.actingAsGenerations.delete(oldest);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const serial = ++generation.issued;
|
|
313
|
+
const startedAt = Date.now();
|
|
314
|
+
const lookup = this.lookupActingAs(appId, userId, options, pair, cacheKey, generation, serial, startedAt);
|
|
315
|
+
if (options.cache !== true)
|
|
316
|
+
return lookup;
|
|
293
317
|
this.actingAsPending.set(cacheKey, lookup);
|
|
294
|
-
|
|
318
|
+
try {
|
|
319
|
+
return await lookup;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
if (this.actingAsPending.get(cacheKey) === lookup)
|
|
323
|
+
this.actingAsPending.delete(cacheKey);
|
|
324
|
+
}
|
|
295
325
|
}
|
|
296
326
|
// ── Internals ────────────────────────────────────────────────────────────
|
|
297
327
|
/**
|
|
@@ -347,7 +377,7 @@ export class OxyServer extends OxyServices {
|
|
|
347
377
|
/** A token obtained by attestation, cached like a credential's (one identity per process). */
|
|
348
378
|
async workloadServiceToken() {
|
|
349
379
|
const entry = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
|
|
350
|
-
if (entry?.token && entry.expiresAt > Date.now() + TOKEN_REUSE_MARGIN_MS)
|
|
380
|
+
if (entry?.token && reusableServiceToken(entry.token) && entry.expiresAt > Date.now() + TOKEN_REUSE_MARGIN_MS)
|
|
351
381
|
return entry.token;
|
|
352
382
|
if (entry?.pending)
|
|
353
383
|
return entry.pending;
|
|
@@ -378,36 +408,62 @@ export class OxyServer extends OxyServices {
|
|
|
378
408
|
settled.pending = null;
|
|
379
409
|
}
|
|
380
410
|
}
|
|
381
|
-
async lookupActingAs(appId, userId) {
|
|
411
|
+
async lookupActingAs(appId, userId, options, pair, cacheKey, generation, serial, startedAt) {
|
|
382
412
|
try {
|
|
383
|
-
// The verify endpoint admits only a platform-trusted caller, so this
|
|
384
|
-
// carries the VERIFIER's own service token. No retry and a short timeout:
|
|
385
|
-
// it runs inside request-handling middleware, and a retry loop would
|
|
386
|
-
// multiply the latency of every delegated request. A verifier with no
|
|
387
|
-
// credential throws here and is refused below — a host that cannot prove
|
|
388
|
-
// who it is has no business learning who delegated to whom.
|
|
389
413
|
const serviceToken = await this.serviceToken();
|
|
390
|
-
const result = await this.request('GET', '/internal/service-acting-as/verify', { appId, userId
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
this.
|
|
414
|
+
const result = await this.request('GET', '/internal/service-acting-as/verify', { appId, userId, ...(options.credentialId ? { credentialId: options.credentialId,
|
|
415
|
+
ownerAccountId: options.ownerAccountId, environment: options.environment } : {}) }, { cache: false, retry: false, timeout: 5000, headers: { Authorization: `Bearer ${serviceToken}` } });
|
|
416
|
+
// A deadline bounds how long an already old response can authorize. It
|
|
417
|
+
// is not by itself a measurement of end-to-end revocation latency.
|
|
418
|
+
if (this.actingAsGenerations.get(pair) !== generation || serial < generation.completed)
|
|
419
|
+
return null;
|
|
420
|
+
generation.completed = serial;
|
|
421
|
+
this.clearActingAsPair(appId, userId);
|
|
422
|
+
if (Date.now() - startedAt >= 5000)
|
|
423
|
+
return null;
|
|
424
|
+
const rawEpoch = result?.epoch;
|
|
425
|
+
if (typeof rawEpoch !== 'string' || !/^(?:0|[1-9][0-9]{0,18})$/.test(rawEpoch))
|
|
426
|
+
return null;
|
|
427
|
+
const epoch = BigInt(rawEpoch);
|
|
428
|
+
if (epoch > 9223372036854775807n)
|
|
429
|
+
return null;
|
|
430
|
+
if (typeof result.authorized !== 'boolean' || !Array.isArray(result.scopes)
|
|
431
|
+
|| !result.scopes.every((scope) => typeof scope === 'string' && scope.length > 0 && scope.trim() === scope)
|
|
432
|
+
|| new Set(result.scopes).size !== result.scopes.length
|
|
433
|
+
|| (!result.authorized && result.scopes.length !== 0))
|
|
434
|
+
return null;
|
|
435
|
+
const older = epoch < generation.epoch;
|
|
436
|
+
if (epoch > generation.epoch)
|
|
437
|
+
generation.epoch = epoch;
|
|
438
|
+
if (result.authorized && older)
|
|
439
|
+
return null;
|
|
440
|
+
const verified = result.authorized === true
|
|
441
|
+
? { authorized: true, scopes: result.scopes, epoch: rawEpoch } : null;
|
|
442
|
+
if (options.cache === true)
|
|
443
|
+
this.rememberActingAs(cacheKey, verified, startedAt + (verified ? ACTING_AS_GRANT_TTL_MS : ACTING_AS_DENIAL_TTL_MS));
|
|
395
444
|
return verified;
|
|
396
445
|
}
|
|
397
446
|
catch (error) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
447
|
+
if (this.actingAsGenerations.get(pair) === generation && serial >= generation.completed) {
|
|
448
|
+
generation.completed = serial;
|
|
449
|
+
this.clearActingAsPair(appId, userId);
|
|
450
|
+
}
|
|
451
|
+
logger.warn('[oxy.auth] verifyActingAs lookup failed — not cached', {
|
|
452
|
+
component: 'auth', method: 'verifyActingAs', appId, userId,
|
|
403
453
|
}, error);
|
|
404
|
-
this.rememberActingAs(`${appId}:${userId}`, null, ACTING_AS_DENIAL_TTL_MS);
|
|
405
454
|
return null;
|
|
406
455
|
}
|
|
407
456
|
}
|
|
408
|
-
|
|
457
|
+
clearActingAsPair(appId, userId) {
|
|
458
|
+
for (const key of this.actingAs.keys()) {
|
|
459
|
+
const [cachedApp, cachedUser] = JSON.parse(key);
|
|
460
|
+
if (cachedApp === appId && cachedUser === userId)
|
|
461
|
+
this.actingAs.delete(key);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
rememberActingAs(cacheKey, result, expiresAt) {
|
|
409
465
|
this.actingAs.delete(cacheKey);
|
|
410
|
-
this.actingAs.set(cacheKey, { result, expiresAt
|
|
466
|
+
this.actingAs.set(cacheKey, { result, expiresAt });
|
|
411
467
|
if (this.actingAs.size > ACTING_AS_CACHE_MAX) {
|
|
412
468
|
const oldest = this.actingAs.keys().next().value;
|
|
413
469
|
if (oldest !== undefined)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Autonomous entry helper. The signer owns its private key (for example in a KMS). */
|
|
2
|
+
import { buildAgentProofMessage } from '@oxy.so/contracts';
|
|
3
|
+
import { normalizeSecp256k1PublicKey } from '@oxy.so/protocol/secp256k1';
|
|
4
|
+
/** Explicit sign-in; no timer, automatic escalation or service credential lane. */
|
|
5
|
+
export async function signInAgentAccount(options) {
|
|
6
|
+
const { client, accountId, signer } = options;
|
|
7
|
+
const publicKey = normalizeSecp256k1PublicKey(signer.publicKey.trim());
|
|
8
|
+
const claims = await client.auth.agent.requestChallenge(publicKey);
|
|
9
|
+
const timestamp = Date.now();
|
|
10
|
+
if (claims.action !== 'agent_signin' || claims.accountId !== accountId || claims.actorId !== accountId
|
|
11
|
+
|| normalizeSecp256k1PublicKey(claims.publicKey) !== publicKey || !claims.authMethodId || claims.expiresAt <= timestamp) {
|
|
12
|
+
throw new Error('Agent challenge does not match this account and signer');
|
|
13
|
+
}
|
|
14
|
+
const signature = await signer.signMessage(buildAgentProofMessage(claims, timestamp));
|
|
15
|
+
return client.auth.agent.verify(publicKey, { challenge: claims.challenge, timestamp, signature });
|
|
16
|
+
}
|