@synap-core/sdk 1.5.1 → 1.5.2
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/AGENTS.md +250 -0
- package/CHANGELOG.md +83 -17
- package/README.md +3 -3
- package/dist/connect.cjs +29 -5
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +39 -3
- package/dist/connect.d.ts +39 -3
- package/dist/connect.js +29 -5
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +68 -9
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +19 -1
- package/dist/testing.d.ts +19 -1
- package/dist/testing.js +68 -9
- package/dist/testing.js.map +1 -1
- package/package.json +10 -9
- package/src/connect/auth.ts +14 -4
- package/src/connect/client.ts +55 -2
- package/src/connect/flow.ts +23 -2
- package/src/connect/index.ts +1 -0
- package/src/testing/auth-server.ts +118 -14
package/dist/connect.d.cts
CHANGED
|
@@ -316,7 +316,27 @@ interface ApproveProposalOptions {
|
|
|
316
316
|
|
|
317
317
|
type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable"
|
|
318
318
|
/** The pod answered a redirect (a moved pod answers 308 from its old name): connect again at its new address. */
|
|
319
|
-
| "pod_moved"
|
|
319
|
+
| "pod_moved"
|
|
320
|
+
/**
|
|
321
|
+
* A credential door (approving a proposal that widens who or what can reach
|
|
322
|
+
* the pod) wants the person to re-confirm at the POD within 15 minutes; a
|
|
323
|
+
* Synap Cloud sign-in never counts. Recovery: `prompt_login`.
|
|
324
|
+
*/
|
|
325
|
+
| "step_up_required"
|
|
326
|
+
/** Synap Cloud refused to vouch a person with no verified email. Recovery: `verify_email`. */
|
|
327
|
+
| "email_unverified"
|
|
328
|
+
/** Synap Cloud vouches an app origin over HTTPS only. Recovery: `use_https`. */
|
|
329
|
+
| "origin_insecure" | "server";
|
|
330
|
+
/**
|
|
331
|
+
* What a typed refusal asks the app to do:
|
|
332
|
+
* - `prompt_login`: send the person through the connect handler with
|
|
333
|
+
* `prompt=login` (re-confirm at the pod; the SDK redeems it with the
|
|
334
|
+
* device's refresh token), then retry ONCE;
|
|
335
|
+
* - `verify_email`: tell the person to verify their Synap email;
|
|
336
|
+
* - `use_https`: serve the app over HTTPS (`http://localhost` signs in to a
|
|
337
|
+
* local Control Plane started with `CP_ALLOW_DEV_ORIGINS=true`, never to api.synap.live).
|
|
338
|
+
*/
|
|
339
|
+
type PodRecovery = "prompt_login" | "verify_email" | "use_https";
|
|
320
340
|
/**
|
|
321
341
|
* Why the SDK refused before sending anything (`status: null`):
|
|
322
342
|
* - `agent_cannot_approve`: an agent connection asked to approve (the pod
|
|
@@ -334,6 +354,8 @@ interface PodError {
|
|
|
334
354
|
detail?: string;
|
|
335
355
|
/** Set when the SDK refused locally (1.5). */
|
|
336
356
|
reason?: PodRefusalReason;
|
|
357
|
+
/** What to do next, on `step_up_required`, `email_unverified` and `origin_insecure`. */
|
|
358
|
+
recovery?: PodRecovery;
|
|
337
359
|
/**
|
|
338
360
|
* On `unauthorized` (1.5): the pod refused the access token, the connection
|
|
339
361
|
* still holds a refresh token, and this client could not renew it (no
|
|
@@ -914,6 +936,8 @@ interface ConnectTransaction {
|
|
|
914
936
|
resource: string | null;
|
|
915
937
|
/** A same-origin path to land on afterwards. */
|
|
916
938
|
next: string | null;
|
|
939
|
+
/** The sign-in re-confirms a device that is already connected (`prompt=login`). */
|
|
940
|
+
stepUp?: boolean;
|
|
917
941
|
createdAt: number;
|
|
918
942
|
}
|
|
919
943
|
interface FlowOptions {
|
|
@@ -938,8 +962,13 @@ interface StartConnectOptions extends FlowOptions {
|
|
|
938
962
|
* `"consent"`: the person asked to see the consent screen again (to widen or
|
|
939
963
|
* narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
|
|
940
964
|
* is otherwise the same (fresh PKCE verifier and state).
|
|
965
|
+
*
|
|
966
|
+
* `"login"`: a step-up. A credential door refused a stale device
|
|
967
|
+
* (`step_up_required`): the person re-confirms at the pod (OIDC
|
|
968
|
+
* `prompt=login`) and the callback redeems the code WITH the connection's
|
|
969
|
+
* refresh token (`synap_device_refresh_token`; see `HandleCallbackOptions.current`).
|
|
941
970
|
*/
|
|
942
|
-
prompt?: "consent" | null;
|
|
971
|
+
prompt?: "consent" | "login" | null;
|
|
943
972
|
allowInsecureLoopback?: boolean;
|
|
944
973
|
}
|
|
945
974
|
interface StartedConnect {
|
|
@@ -964,6 +993,13 @@ interface HandleCallbackOptions extends FlowOptions {
|
|
|
964
993
|
clientId: string;
|
|
965
994
|
redirectUri: string;
|
|
966
995
|
secret: AppSecret;
|
|
996
|
+
/**
|
|
997
|
+
* The connection this browser holds now. A step-up (`prompt=login`) of a
|
|
998
|
+
* Synap app's device is redeemed with ITS refresh token (the pod moves that
|
|
999
|
+
* device's clock instead of making a second device); without one, or for an
|
|
1000
|
+
* app that is not a person's device, the code is redeemed as any other.
|
|
1001
|
+
*/
|
|
1002
|
+
current?: PodConnection | null;
|
|
967
1003
|
}
|
|
968
1004
|
interface CallbackResult {
|
|
969
1005
|
connection: PodConnection;
|
|
@@ -1028,4 +1064,4 @@ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions)
|
|
|
1028
1064
|
/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
|
|
1029
1065
|
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
1030
1066
|
|
|
1031
|
-
export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, type ApproveProposalOptions, CLOUD_TRANSACTION_TTL_MS, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, type CloudConnectConfig, type CloudReturn, type CloudTransaction, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type GetDocumentOptions, type GetEntityConnectionsOptions, type HandleCallbackOptions, type LaunchHint, type ListEntitiesOptions, type ListFocusSessionsOptions, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, PodActsAs, type PodAppAuth, type PodAppAuthConfig, type PodAppClientOptions, type PodAppPaths, type PodApproval, type PodClient, type PodClientOptions, PodConnection, type PodConnectionSource, type PodDocument, type PodDocumentSection, type PodEntity, type PodEntityConnection, type PodEntityConnections, type PodEntityFacet, type PodError, type PodErrorCode, type PodFileUrl, type PodFocusSession, PodGrant, PodIssuer, type PodRefusalReason, type PodResult, type PodSessionSlot, type PodSessionStatus, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOptions, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCloudTransaction, type SealedCookieStoreOptions, type SealedTransaction, type StartCloudConnectOptions, type StartConnectOptions, type StartedCloudConnect, type StartedConnect, TRANSACTION_TTL_MS, acceptCloudReturn, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudBaseUrl, cloudConnectAvailable, connectionActsAs, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, humanSlots, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };
|
|
1067
|
+
export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, type ApproveProposalOptions, CLOUD_TRANSACTION_TTL_MS, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, type CloudConnectConfig, type CloudReturn, type CloudTransaction, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type GetDocumentOptions, type GetEntityConnectionsOptions, type HandleCallbackOptions, type LaunchHint, type ListEntitiesOptions, type ListFocusSessionsOptions, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, PodActsAs, type PodAppAuth, type PodAppAuthConfig, type PodAppClientOptions, type PodAppPaths, type PodApproval, type PodClient, type PodClientOptions, PodConnection, type PodConnectionSource, type PodDocument, type PodDocumentSection, type PodEntity, type PodEntityConnection, type PodEntityConnections, type PodEntityFacet, type PodError, type PodErrorCode, type PodFileUrl, type PodFocusSession, PodGrant, PodIssuer, type PodRecovery, type PodRefusalReason, type PodResult, type PodSessionSlot, type PodSessionStatus, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOptions, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCloudTransaction, type SealedCookieStoreOptions, type SealedTransaction, type StartCloudConnectOptions, type StartConnectOptions, type StartedCloudConnect, type StartedConnect, TRANSACTION_TTL_MS, acceptCloudReturn, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudBaseUrl, cloudConnectAvailable, connectionActsAs, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, humanSlots, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };
|
package/dist/connect.d.ts
CHANGED
|
@@ -316,7 +316,27 @@ interface ApproveProposalOptions {
|
|
|
316
316
|
|
|
317
317
|
type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable"
|
|
318
318
|
/** The pod answered a redirect (a moved pod answers 308 from its old name): connect again at its new address. */
|
|
319
|
-
| "pod_moved"
|
|
319
|
+
| "pod_moved"
|
|
320
|
+
/**
|
|
321
|
+
* A credential door (approving a proposal that widens who or what can reach
|
|
322
|
+
* the pod) wants the person to re-confirm at the POD within 15 minutes; a
|
|
323
|
+
* Synap Cloud sign-in never counts. Recovery: `prompt_login`.
|
|
324
|
+
*/
|
|
325
|
+
| "step_up_required"
|
|
326
|
+
/** Synap Cloud refused to vouch a person with no verified email. Recovery: `verify_email`. */
|
|
327
|
+
| "email_unverified"
|
|
328
|
+
/** Synap Cloud vouches an app origin over HTTPS only. Recovery: `use_https`. */
|
|
329
|
+
| "origin_insecure" | "server";
|
|
330
|
+
/**
|
|
331
|
+
* What a typed refusal asks the app to do:
|
|
332
|
+
* - `prompt_login`: send the person through the connect handler with
|
|
333
|
+
* `prompt=login` (re-confirm at the pod; the SDK redeems it with the
|
|
334
|
+
* device's refresh token), then retry ONCE;
|
|
335
|
+
* - `verify_email`: tell the person to verify their Synap email;
|
|
336
|
+
* - `use_https`: serve the app over HTTPS (`http://localhost` signs in to a
|
|
337
|
+
* local Control Plane started with `CP_ALLOW_DEV_ORIGINS=true`, never to api.synap.live).
|
|
338
|
+
*/
|
|
339
|
+
type PodRecovery = "prompt_login" | "verify_email" | "use_https";
|
|
320
340
|
/**
|
|
321
341
|
* Why the SDK refused before sending anything (`status: null`):
|
|
322
342
|
* - `agent_cannot_approve`: an agent connection asked to approve (the pod
|
|
@@ -334,6 +354,8 @@ interface PodError {
|
|
|
334
354
|
detail?: string;
|
|
335
355
|
/** Set when the SDK refused locally (1.5). */
|
|
336
356
|
reason?: PodRefusalReason;
|
|
357
|
+
/** What to do next, on `step_up_required`, `email_unverified` and `origin_insecure`. */
|
|
358
|
+
recovery?: PodRecovery;
|
|
337
359
|
/**
|
|
338
360
|
* On `unauthorized` (1.5): the pod refused the access token, the connection
|
|
339
361
|
* still holds a refresh token, and this client could not renew it (no
|
|
@@ -914,6 +936,8 @@ interface ConnectTransaction {
|
|
|
914
936
|
resource: string | null;
|
|
915
937
|
/** A same-origin path to land on afterwards. */
|
|
916
938
|
next: string | null;
|
|
939
|
+
/** The sign-in re-confirms a device that is already connected (`prompt=login`). */
|
|
940
|
+
stepUp?: boolean;
|
|
917
941
|
createdAt: number;
|
|
918
942
|
}
|
|
919
943
|
interface FlowOptions {
|
|
@@ -938,8 +962,13 @@ interface StartConnectOptions extends FlowOptions {
|
|
|
938
962
|
* `"consent"`: the person asked to see the consent screen again (to widen or
|
|
939
963
|
* narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
|
|
940
964
|
* is otherwise the same (fresh PKCE verifier and state).
|
|
965
|
+
*
|
|
966
|
+
* `"login"`: a step-up. A credential door refused a stale device
|
|
967
|
+
* (`step_up_required`): the person re-confirms at the pod (OIDC
|
|
968
|
+
* `prompt=login`) and the callback redeems the code WITH the connection's
|
|
969
|
+
* refresh token (`synap_device_refresh_token`; see `HandleCallbackOptions.current`).
|
|
941
970
|
*/
|
|
942
|
-
prompt?: "consent" | null;
|
|
971
|
+
prompt?: "consent" | "login" | null;
|
|
943
972
|
allowInsecureLoopback?: boolean;
|
|
944
973
|
}
|
|
945
974
|
interface StartedConnect {
|
|
@@ -964,6 +993,13 @@ interface HandleCallbackOptions extends FlowOptions {
|
|
|
964
993
|
clientId: string;
|
|
965
994
|
redirectUri: string;
|
|
966
995
|
secret: AppSecret;
|
|
996
|
+
/**
|
|
997
|
+
* The connection this browser holds now. A step-up (`prompt=login`) of a
|
|
998
|
+
* Synap app's device is redeemed with ITS refresh token (the pod moves that
|
|
999
|
+
* device's clock instead of making a second device); without one, or for an
|
|
1000
|
+
* app that is not a person's device, the code is redeemed as any other.
|
|
1001
|
+
*/
|
|
1002
|
+
current?: PodConnection | null;
|
|
967
1003
|
}
|
|
968
1004
|
interface CallbackResult {
|
|
969
1005
|
connection: PodConnection;
|
|
@@ -1028,4 +1064,4 @@ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions)
|
|
|
1028
1064
|
/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
|
|
1029
1065
|
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
1030
1066
|
|
|
1031
|
-
export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, type ApproveProposalOptions, CLOUD_TRANSACTION_TTL_MS, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, type CloudConnectConfig, type CloudReturn, type CloudTransaction, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type GetDocumentOptions, type GetEntityConnectionsOptions, type HandleCallbackOptions, type LaunchHint, type ListEntitiesOptions, type ListFocusSessionsOptions, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, PodActsAs, type PodAppAuth, type PodAppAuthConfig, type PodAppClientOptions, type PodAppPaths, type PodApproval, type PodClient, type PodClientOptions, PodConnection, type PodConnectionSource, type PodDocument, type PodDocumentSection, type PodEntity, type PodEntityConnection, type PodEntityConnections, type PodEntityFacet, type PodError, type PodErrorCode, type PodFileUrl, type PodFocusSession, PodGrant, PodIssuer, type PodRefusalReason, type PodResult, type PodSessionSlot, type PodSessionStatus, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOptions, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCloudTransaction, type SealedCookieStoreOptions, type SealedTransaction, type StartCloudConnectOptions, type StartConnectOptions, type StartedCloudConnect, type StartedConnect, TRANSACTION_TTL_MS, acceptCloudReturn, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudBaseUrl, cloudConnectAvailable, connectionActsAs, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, humanSlots, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };
|
|
1067
|
+
export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, type ApproveProposalOptions, CLOUD_TRANSACTION_TTL_MS, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, type CloudConnectConfig, type CloudReturn, type CloudTransaction, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type GetDocumentOptions, type GetEntityConnectionsOptions, type HandleCallbackOptions, type LaunchHint, type ListEntitiesOptions, type ListFocusSessionsOptions, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, PodActsAs, type PodAppAuth, type PodAppAuthConfig, type PodAppClientOptions, type PodAppPaths, type PodApproval, type PodClient, type PodClientOptions, PodConnection, type PodConnectionSource, type PodDocument, type PodDocumentSection, type PodEntity, type PodEntityConnection, type PodEntityConnections, type PodEntityFacet, type PodError, type PodErrorCode, type PodFileUrl, type PodFocusSession, PodGrant, PodIssuer, type PodRecovery, type PodRefusalReason, type PodResult, type PodSessionSlot, type PodSessionStatus, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOptions, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCloudTransaction, type SealedCookieStoreOptions, type SealedTransaction, type StartCloudConnectOptions, type StartConnectOptions, type StartedCloudConnect, type StartedConnect, TRANSACTION_TTL_MS, acceptCloudReturn, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudBaseUrl, cloudConnectAvailable, connectionActsAs, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, humanSlots, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };
|
package/dist/connect.js
CHANGED
|
@@ -555,14 +555,32 @@ function renewalError(err) {
|
|
|
555
555
|
return { code: "server", status: null, detail };
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
|
+
var STEP_UP_MESSAGE = "Confirm it's you before changing who or what can reach this pod.";
|
|
559
|
+
var CP_PROBLEMS = {
|
|
560
|
+
CP_ACCOUNT_EMAIL_REQUIRED: { code: "email_unverified", recovery: "verify_email" },
|
|
561
|
+
FEDERATED_APPLICATION_ORIGIN_INSECURE: { code: "origin_insecure", recovery: "use_https" }
|
|
562
|
+
};
|
|
563
|
+
function isStepUp(obj) {
|
|
564
|
+
const data = (v) => v && typeof v === "object" ? v : {};
|
|
565
|
+
const nested = data(data(obj.error).data).reasonCode ?? data(obj.data).reasonCode ?? obj.reasonCode;
|
|
566
|
+
return nested === "STEP_UP_REQUIRED" || obj.error === STEP_UP_MESSAGE || obj.message === STEP_UP_MESSAGE;
|
|
567
|
+
}
|
|
568
|
+
function classifyRefusal(status, body) {
|
|
569
|
+
const obj = body && typeof body === "object" ? body : {};
|
|
570
|
+
if (status === 403) {
|
|
571
|
+
if (isStepUp(obj)) return { code: "step_up_required", recovery: "prompt_login" };
|
|
572
|
+
return { code: obj.code === "GRANT_DOOR_UNSUPPORTED" ? "door_unsupported" : "forbidden" };
|
|
573
|
+
}
|
|
574
|
+
const problem = typeof obj.code === "string" ? CP_PROBLEMS[obj.code] : void 0;
|
|
575
|
+
return problem ?? { code: "conflict" };
|
|
576
|
+
}
|
|
558
577
|
function errorFor(status, body, retryAfter, now) {
|
|
559
578
|
const obj = body && typeof body === "object" ? body : {};
|
|
560
579
|
const detail = typeof obj.error === "string" ? obj.error : typeof obj.message === "string" ? obj.message : void 0;
|
|
561
580
|
const base = { status, ...detail !== void 0 ? { detail } : {} };
|
|
562
581
|
if (status === 401) return { code: "unauthorized", ...base };
|
|
563
|
-
if (status === 403
|
|
582
|
+
if (status === 403 || status === 409) return { ...classifyRefusal(status, body), ...base };
|
|
564
583
|
if (status === 404) return { code: "not_found", ...base };
|
|
565
|
-
if (status === 409) return { code: "conflict", ...base };
|
|
566
584
|
if (status === 400 || status === 422) return { code: "invalid", ...base };
|
|
567
585
|
if (status === 429) {
|
|
568
586
|
const retryAfterSec = retryAfterSeconds(retryAfter, now);
|
|
@@ -898,7 +916,7 @@ async function startConnect(opts) {
|
|
|
898
916
|
url.searchParams.set("code_challenge_method", "S256");
|
|
899
917
|
if (resource) url.searchParams.set("resource", resource);
|
|
900
918
|
if (opts.spaceHint) url.searchParams.set("synap_space_hint", opts.spaceHint);
|
|
901
|
-
if (opts.prompt === "consent") url.searchParams.set("prompt",
|
|
919
|
+
if (opts.prompt === "consent" || opts.prompt === "login") url.searchParams.set("prompt", opts.prompt);
|
|
902
920
|
const tx = {
|
|
903
921
|
v: 1,
|
|
904
922
|
issuer: issuer.issuer,
|
|
@@ -910,6 +928,7 @@ async function startConnect(opts) {
|
|
|
910
928
|
verifier,
|
|
911
929
|
resource,
|
|
912
930
|
next: opts.next ?? null,
|
|
931
|
+
...opts.prompt === "login" ? { stepUp: true } : {},
|
|
913
932
|
createdAt: now
|
|
914
933
|
};
|
|
915
934
|
const expiresAt = now + TRANSACTION_TTL_MS;
|
|
@@ -967,13 +986,16 @@ async function handleCallback(opts) {
|
|
|
967
986
|
code_verifier: tx.verifier
|
|
968
987
|
});
|
|
969
988
|
if (tx.resource) body.set("resource", tx.resource);
|
|
989
|
+
const device = tx.stepUp && opts.current && opts.current.actsAs === "person" && opts.current.podUrl === tx.issuer && opts.current.clientId === tx.clientId ? opts.current : null;
|
|
990
|
+
if (device?.refreshToken) body.set("synap_device_refresh_token", device.refreshToken);
|
|
970
991
|
const json = await tokenRequest(tx.tokenEndpoint, body, "token", opts);
|
|
971
992
|
const connection = parseTokenResponse(json, {
|
|
972
993
|
podUrl: tx.issuer,
|
|
973
994
|
clientId: tx.clientId,
|
|
974
995
|
tokenEndpoint: tx.tokenEndpoint,
|
|
975
996
|
revocationEndpoint: tx.revocationEndpoint,
|
|
976
|
-
now: (opts.now ?? Date.now)()
|
|
997
|
+
now: (opts.now ?? Date.now)(),
|
|
998
|
+
...device ? { previous: device } : {}
|
|
977
999
|
});
|
|
978
1000
|
return { connection, next: tx.next };
|
|
979
1001
|
}
|
|
@@ -1479,7 +1501,8 @@ function createPodAppAuth(config) {
|
|
|
1479
1501
|
const pod = formField(form, "pod")?.slice(0, 512) ?? "";
|
|
1480
1502
|
const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
|
|
1481
1503
|
const space = formField(form, "space");
|
|
1482
|
-
const
|
|
1504
|
+
const asked = formField(form, "prompt");
|
|
1505
|
+
const prompt = asked === "consent" || asked === "login" ? asked : null;
|
|
1483
1506
|
try {
|
|
1484
1507
|
if (!prompt && await liveConnectionTo(request, pod)) return redirect(new URL(next, origin).href);
|
|
1485
1508
|
return await toPod({ pod, next, space, prompt });
|
|
@@ -1496,6 +1519,7 @@ function createPodAppAuth(config) {
|
|
|
1496
1519
|
...flow,
|
|
1497
1520
|
callbackUrl: request.url,
|
|
1498
1521
|
transaction,
|
|
1522
|
+
current: await store.read(request).catch(() => null),
|
|
1499
1523
|
clientId,
|
|
1500
1524
|
redirectUri,
|
|
1501
1525
|
secret: config.secret
|