@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.
@@ -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" | "server";
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" | "server";
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) return { code: obj.code === "GRANT_DOOR_UNSUPPORTED" ? "door_unsupported" : "forbidden", ...base };
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", "consent");
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 prompt = formField(form, "prompt") === "consent" ? "consent" : null;
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