@synap-core/sdk 1.5.0 → 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 +148 -0
- package/README.md +17 -1
- package/dist/connect.cjs +38 -7
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +55 -3
- package/dist/connect.d.ts +55 -3
- package/dist/connect.js +38 -7
- package/dist/connect.js.map +1 -1
- package/dist/testing.cjs +188 -16
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +69 -2
- package/dist/testing.d.ts +69 -2
- package/dist/testing.js +188 -16
- package/dist/testing.js.map +1 -1
- package/package.json +10 -9
- package/src/connect/auth.ts +29 -6
- package/src/connect/client.ts +58 -2
- package/src/connect/flow.ts +23 -2
- package/src/connect/hub-types.ts +17 -0
- package/src/connect/index.ts +2 -0
- package/src/testing/auth-server.ts +230 -18
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +78 -2
package/dist/connect.d.cts
CHANGED
|
@@ -94,8 +94,20 @@ interface PodEntity {
|
|
|
94
94
|
/** ISO 8601. */
|
|
95
95
|
createdAt?: string;
|
|
96
96
|
updatedAt?: string;
|
|
97
|
+
/** The roles it wears, with `includeFacets`; absent when it wears none. */
|
|
98
|
+
facetSlugs?: string[];
|
|
99
|
+
/** Each role's overlay, with `includeFacets`; absent when it wears none. */
|
|
100
|
+
facets?: PodEntityFacet[];
|
|
97
101
|
[key: string]: unknown;
|
|
98
102
|
}
|
|
103
|
+
/** One role an entity wears (`FacetRowAnnotation`). */
|
|
104
|
+
interface PodEntityFacet {
|
|
105
|
+
facetId: string;
|
|
106
|
+
/** The role, e.g. `lead`. */
|
|
107
|
+
slug: string;
|
|
108
|
+
properties: Record<string, unknown>;
|
|
109
|
+
status: string | null;
|
|
110
|
+
}
|
|
99
111
|
interface ListEntitiesOptions {
|
|
100
112
|
/** One granted space. Omitted: every space the connection can read. */
|
|
101
113
|
workspaceId?: string;
|
|
@@ -108,6 +120,10 @@ interface ListEntitiesOptions {
|
|
|
108
120
|
sort?: string;
|
|
109
121
|
/** With `workspaceId`: also the person's pod-wide rows (the pod's default: true). */
|
|
110
122
|
includePodWide?: boolean;
|
|
123
|
+
/** Only the entities wearing this role, e.g. `lead`. */
|
|
124
|
+
facetSlug?: string;
|
|
125
|
+
/** Put `facetSlugs` and `facets` on each row that wears a role. */
|
|
126
|
+
includeFacets?: boolean;
|
|
111
127
|
}
|
|
112
128
|
type PodConnectionSource = "graph" | "property" | "thread" | "context_channel" | "focus_session";
|
|
113
129
|
/** One edge of `GET /entities/:id/connections` (`EntityConnection`). */
|
|
@@ -300,7 +316,27 @@ interface ApproveProposalOptions {
|
|
|
300
316
|
|
|
301
317
|
type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable"
|
|
302
318
|
/** The pod answered a redirect (a moved pod answers 308 from its old name): connect again at its new address. */
|
|
303
|
-
| "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";
|
|
304
340
|
/**
|
|
305
341
|
* Why the SDK refused before sending anything (`status: null`):
|
|
306
342
|
* - `agent_cannot_approve`: an agent connection asked to approve (the pod
|
|
@@ -318,6 +354,8 @@ interface PodError {
|
|
|
318
354
|
detail?: string;
|
|
319
355
|
/** Set when the SDK refused locally (1.5). */
|
|
320
356
|
reason?: PodRefusalReason;
|
|
357
|
+
/** What to do next, on `step_up_required`, `email_unverified` and `origin_insecure`. */
|
|
358
|
+
recovery?: PodRecovery;
|
|
321
359
|
/**
|
|
322
360
|
* On `unauthorized` (1.5): the pod refused the access token, the connection
|
|
323
361
|
* still holds a refresh token, and this client could not renew it (no
|
|
@@ -898,6 +936,8 @@ interface ConnectTransaction {
|
|
|
898
936
|
resource: string | null;
|
|
899
937
|
/** A same-origin path to land on afterwards. */
|
|
900
938
|
next: string | null;
|
|
939
|
+
/** The sign-in re-confirms a device that is already connected (`prompt=login`). */
|
|
940
|
+
stepUp?: boolean;
|
|
901
941
|
createdAt: number;
|
|
902
942
|
}
|
|
903
943
|
interface FlowOptions {
|
|
@@ -922,8 +962,13 @@ interface StartConnectOptions extends FlowOptions {
|
|
|
922
962
|
* `"consent"`: the person asked to see the consent screen again (to widen or
|
|
923
963
|
* narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
|
|
924
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`).
|
|
925
970
|
*/
|
|
926
|
-
prompt?: "consent" | null;
|
|
971
|
+
prompt?: "consent" | "login" | null;
|
|
927
972
|
allowInsecureLoopback?: boolean;
|
|
928
973
|
}
|
|
929
974
|
interface StartedConnect {
|
|
@@ -948,6 +993,13 @@ interface HandleCallbackOptions extends FlowOptions {
|
|
|
948
993
|
clientId: string;
|
|
949
994
|
redirectUri: string;
|
|
950
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;
|
|
951
1003
|
}
|
|
952
1004
|
interface CallbackResult {
|
|
953
1005
|
connection: PodConnection;
|
|
@@ -1012,4 +1064,4 @@ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions)
|
|
|
1012
1064
|
/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
|
|
1013
1065
|
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
1014
1066
|
|
|
1015
|
-
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 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
|
@@ -94,8 +94,20 @@ interface PodEntity {
|
|
|
94
94
|
/** ISO 8601. */
|
|
95
95
|
createdAt?: string;
|
|
96
96
|
updatedAt?: string;
|
|
97
|
+
/** The roles it wears, with `includeFacets`; absent when it wears none. */
|
|
98
|
+
facetSlugs?: string[];
|
|
99
|
+
/** Each role's overlay, with `includeFacets`; absent when it wears none. */
|
|
100
|
+
facets?: PodEntityFacet[];
|
|
97
101
|
[key: string]: unknown;
|
|
98
102
|
}
|
|
103
|
+
/** One role an entity wears (`FacetRowAnnotation`). */
|
|
104
|
+
interface PodEntityFacet {
|
|
105
|
+
facetId: string;
|
|
106
|
+
/** The role, e.g. `lead`. */
|
|
107
|
+
slug: string;
|
|
108
|
+
properties: Record<string, unknown>;
|
|
109
|
+
status: string | null;
|
|
110
|
+
}
|
|
99
111
|
interface ListEntitiesOptions {
|
|
100
112
|
/** One granted space. Omitted: every space the connection can read. */
|
|
101
113
|
workspaceId?: string;
|
|
@@ -108,6 +120,10 @@ interface ListEntitiesOptions {
|
|
|
108
120
|
sort?: string;
|
|
109
121
|
/** With `workspaceId`: also the person's pod-wide rows (the pod's default: true). */
|
|
110
122
|
includePodWide?: boolean;
|
|
123
|
+
/** Only the entities wearing this role, e.g. `lead`. */
|
|
124
|
+
facetSlug?: string;
|
|
125
|
+
/** Put `facetSlugs` and `facets` on each row that wears a role. */
|
|
126
|
+
includeFacets?: boolean;
|
|
111
127
|
}
|
|
112
128
|
type PodConnectionSource = "graph" | "property" | "thread" | "context_channel" | "focus_session";
|
|
113
129
|
/** One edge of `GET /entities/:id/connections` (`EntityConnection`). */
|
|
@@ -300,7 +316,27 @@ interface ApproveProposalOptions {
|
|
|
300
316
|
|
|
301
317
|
type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable"
|
|
302
318
|
/** The pod answered a redirect (a moved pod answers 308 from its old name): connect again at its new address. */
|
|
303
|
-
| "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";
|
|
304
340
|
/**
|
|
305
341
|
* Why the SDK refused before sending anything (`status: null`):
|
|
306
342
|
* - `agent_cannot_approve`: an agent connection asked to approve (the pod
|
|
@@ -318,6 +354,8 @@ interface PodError {
|
|
|
318
354
|
detail?: string;
|
|
319
355
|
/** Set when the SDK refused locally (1.5). */
|
|
320
356
|
reason?: PodRefusalReason;
|
|
357
|
+
/** What to do next, on `step_up_required`, `email_unverified` and `origin_insecure`. */
|
|
358
|
+
recovery?: PodRecovery;
|
|
321
359
|
/**
|
|
322
360
|
* On `unauthorized` (1.5): the pod refused the access token, the connection
|
|
323
361
|
* still holds a refresh token, and this client could not renew it (no
|
|
@@ -898,6 +936,8 @@ interface ConnectTransaction {
|
|
|
898
936
|
resource: string | null;
|
|
899
937
|
/** A same-origin path to land on afterwards. */
|
|
900
938
|
next: string | null;
|
|
939
|
+
/** The sign-in re-confirms a device that is already connected (`prompt=login`). */
|
|
940
|
+
stepUp?: boolean;
|
|
901
941
|
createdAt: number;
|
|
902
942
|
}
|
|
903
943
|
interface FlowOptions {
|
|
@@ -922,8 +962,13 @@ interface StartConnectOptions extends FlowOptions {
|
|
|
922
962
|
* `"consent"`: the person asked to see the consent screen again (to widen or
|
|
923
963
|
* narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
|
|
924
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`).
|
|
925
970
|
*/
|
|
926
|
-
prompt?: "consent" | null;
|
|
971
|
+
prompt?: "consent" | "login" | null;
|
|
927
972
|
allowInsecureLoopback?: boolean;
|
|
928
973
|
}
|
|
929
974
|
interface StartedConnect {
|
|
@@ -948,6 +993,13 @@ interface HandleCallbackOptions extends FlowOptions {
|
|
|
948
993
|
clientId: string;
|
|
949
994
|
redirectUri: string;
|
|
950
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;
|
|
951
1003
|
}
|
|
952
1004
|
interface CallbackResult {
|
|
953
1005
|
connection: PodConnection;
|
|
@@ -1012,4 +1064,4 @@ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions)
|
|
|
1012
1064
|
/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
|
|
1013
1065
|
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
1014
1066
|
|
|
1015
|
-
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 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);
|
|
@@ -733,7 +751,10 @@ function createPodClient(connection, opts = {}) {
|
|
|
733
751
|
limit: clamp(opts2.limit, 1, 1e3),
|
|
734
752
|
offset: clamp(opts2.offset, 0, Number.MAX_SAFE_INTEGER),
|
|
735
753
|
sort: opts2.sort,
|
|
736
|
-
includePodWide: opts2.includePodWide
|
|
754
|
+
includePodWide: opts2.includePodWide,
|
|
755
|
+
facetSlug: opts2.facetSlug,
|
|
756
|
+
// Sent only when asked: the default request stays the 1.5.0 one.
|
|
757
|
+
includeFacets: opts2.includeFacets ? true : void 0
|
|
737
758
|
}, (data) => Array.isArray(data) ? data.filter(hasId) : null);
|
|
738
759
|
}
|
|
739
760
|
async function getEntity(id) {
|
|
@@ -895,7 +916,7 @@ async function startConnect(opts) {
|
|
|
895
916
|
url.searchParams.set("code_challenge_method", "S256");
|
|
896
917
|
if (resource) url.searchParams.set("resource", resource);
|
|
897
918
|
if (opts.spaceHint) url.searchParams.set("synap_space_hint", opts.spaceHint);
|
|
898
|
-
if (opts.prompt === "consent") url.searchParams.set("prompt",
|
|
919
|
+
if (opts.prompt === "consent" || opts.prompt === "login") url.searchParams.set("prompt", opts.prompt);
|
|
899
920
|
const tx = {
|
|
900
921
|
v: 1,
|
|
901
922
|
issuer: issuer.issuer,
|
|
@@ -907,6 +928,7 @@ async function startConnect(opts) {
|
|
|
907
928
|
verifier,
|
|
908
929
|
resource,
|
|
909
930
|
next: opts.next ?? null,
|
|
931
|
+
...opts.prompt === "login" ? { stepUp: true } : {},
|
|
910
932
|
createdAt: now
|
|
911
933
|
};
|
|
912
934
|
const expiresAt = now + TRANSACTION_TTL_MS;
|
|
@@ -964,13 +986,16 @@ async function handleCallback(opts) {
|
|
|
964
986
|
code_verifier: tx.verifier
|
|
965
987
|
});
|
|
966
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);
|
|
967
991
|
const json = await tokenRequest(tx.tokenEndpoint, body, "token", opts);
|
|
968
992
|
const connection = parseTokenResponse(json, {
|
|
969
993
|
podUrl: tx.issuer,
|
|
970
994
|
clientId: tx.clientId,
|
|
971
995
|
tokenEndpoint: tx.tokenEndpoint,
|
|
972
996
|
revocationEndpoint: tx.revocationEndpoint,
|
|
973
|
-
now: (opts.now ?? Date.now)()
|
|
997
|
+
now: (opts.now ?? Date.now)(),
|
|
998
|
+
...device ? { previous: device } : {}
|
|
974
999
|
});
|
|
975
1000
|
return { connection, next: tx.next };
|
|
976
1001
|
}
|
|
@@ -1297,6 +1322,10 @@ var PERMISSION_SEGMENT_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
|
1297
1322
|
function isSynapAppOrigin(origin) {
|
|
1298
1323
|
return /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?\.synap\.live$/.test(origin);
|
|
1299
1324
|
}
|
|
1325
|
+
function isMockModeOrigin(origin) {
|
|
1326
|
+
const url = new URL(origin);
|
|
1327
|
+
return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
1328
|
+
}
|
|
1300
1329
|
function assertAppPermission(permission, origin) {
|
|
1301
1330
|
const segs = permission.split(".");
|
|
1302
1331
|
const fail = (why) => {
|
|
@@ -1306,7 +1335,7 @@ function assertAppPermission(permission, origin) {
|
|
|
1306
1335
|
fail("is not <subject>[.<kind>].<action> in lowercase (no *)");
|
|
1307
1336
|
}
|
|
1308
1337
|
if (segs.length === 3 && segs[0] !== "entity") fail(`names a kind, but "${segs[0]}" has none (only entity does)`);
|
|
1309
|
-
if (SYNAP_APP_REVIEW_PERMISSIONS.includes(permission) && isSynapAppOrigin(origin)) return;
|
|
1338
|
+
if (SYNAP_APP_REVIEW_PERMISSIONS.includes(permission) && (isSynapAppOrigin(origin) || isMockModeOrigin(origin))) return;
|
|
1310
1339
|
if (!GRANT_ACTIONS.includes(segs[segs.length - 1])) {
|
|
1311
1340
|
fail(`asks for "${segs[segs.length - 1]}", which an app cannot be granted (read, create, update, delete)`);
|
|
1312
1341
|
}
|
|
@@ -1472,7 +1501,8 @@ function createPodAppAuth(config) {
|
|
|
1472
1501
|
const pod = formField(form, "pod")?.slice(0, 512) ?? "";
|
|
1473
1502
|
const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
|
|
1474
1503
|
const space = formField(form, "space");
|
|
1475
|
-
const
|
|
1504
|
+
const asked = formField(form, "prompt");
|
|
1505
|
+
const prompt = asked === "consent" || asked === "login" ? asked : null;
|
|
1476
1506
|
try {
|
|
1477
1507
|
if (!prompt && await liveConnectionTo(request, pod)) return redirect(new URL(next, origin).href);
|
|
1478
1508
|
return await toPod({ pod, next, space, prompt });
|
|
@@ -1489,6 +1519,7 @@ function createPodAppAuth(config) {
|
|
|
1489
1519
|
...flow,
|
|
1490
1520
|
callbackUrl: request.url,
|
|
1491
1521
|
transaction,
|
|
1522
|
+
current: await store.read(request).catch(() => null),
|
|
1492
1523
|
clientId,
|
|
1493
1524
|
redirectUri,
|
|
1494
1525
|
secret: config.secret
|