@synap-core/sdk 1.3.0 → 1.4.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.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`, wave C1)
3
+ * for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
4
+ * `choose()` stands in for the person signing in to the CP and picking a pod.
5
+ *
6
+ * Wire behaviour mirrors the CP route (doc 18 §1.1): it accepts only
7
+ * `client_id` (an https CIMD URL; http on loopback only with
8
+ * `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
9
+ * `state` (`^[A-Za-z0-9._~-]{16,256}$`),
10
+ * takes the return URL from the app's client metadata (`initiate_login_uri`),
11
+ * never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
12
+ * It never sees a code or a token.
13
+ *
14
+ * const cloud = createMockCloud();
15
+ * createPodAppAuth({ ...config, cloudUrl: cloud.url });
16
+ * cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
17
+ */
18
+ import type { ClientMetadataDocument } from "../connect/types";
19
+
20
+ /** A placeholder Control Plane origin; never contacted. */
21
+ export const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
22
+
23
+ const STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
24
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
25
+
26
+ export interface MockCloud {
27
+ url: string;
28
+ /** The chooser URLs the app sent browsers to. */
29
+ requests: string[];
30
+ /**
31
+ * The person picks `pod` on the chooser: validates the request like the CP,
32
+ * returns where the CP redirects the browser. Throws on a request the CP
33
+ * refuses (shown as a page there, never redirected).
34
+ */
35
+ choose(location: string, opts: { pod: string; metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri"> }): string;
36
+ /** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
37
+ cancel(location: string, opts: { metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri"> }): string;
38
+ }
39
+
40
+ export function createMockCloud(options: { url?: string; allowInsecureLoopback?: boolean } = {}): MockCloud {
41
+ const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
42
+ const requests: string[] = [];
43
+ return {
44
+ url: base,
45
+ requests,
46
+ choose(location, { pod, metadata }) {
47
+ const back = check(location, metadata);
48
+ back.searchParams.set("iss", pod);
49
+ back.searchParams.set("state", new URL(location).searchParams.get("state")!);
50
+ return back.href;
51
+ },
52
+ cancel(location, { metadata }) {
53
+ const back = check(location, metadata);
54
+ back.searchParams.set("state", new URL(location).searchParams.get("state")!);
55
+ back.searchParams.set("error", "access_denied");
56
+ return back.href;
57
+ },
58
+ };
59
+
60
+ /** Validates like the CP (an HTML error there, never a redirect); returns the app's initiate_login_uri. */
61
+ function check(location: string, metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">): URL {
62
+ requests.push(location);
63
+ const url = new URL(location);
64
+ if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
65
+ const clientId = url.searchParams.get("client_id");
66
+ const state = url.searchParams.get("state");
67
+ if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
68
+ if (!state || !STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
69
+ if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
70
+ return new URL(metadata.initiate_login_uri);
71
+ }
72
+
73
+ /** https; http only on a loopback host, and only with `allowInsecureLoopback`. */
74
+ function cimdUrlAccepted(clientId: string): boolean {
75
+ let url: URL;
76
+ try {
77
+ url = new URL(clientId);
78
+ } catch {
79
+ return false;
80
+ }
81
+ if (url.protocol === "https:") return true;
82
+ return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
83
+ }
84
+ }
@@ -17,7 +17,7 @@
17
17
  * anything else the uniform 404
18
18
  *
19
19
  * Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
20
- * ./auth-server.ts.
20
+ * ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
21
21
  */
22
22
  import type {
23
23
  PublicCollectionPage,
@@ -33,6 +33,8 @@ export { MOCK_FORM, MOCK_SHARE, MOCK_SITE };
33
33
  export type { MockCollection, MockSite } from "./fixtures";
34
34
  // Pod-app sign-in: a mock authorization server + Hub REST doors.
35
35
  export * from "./auth-server";
36
+ // Synap Cloud: a mock Control Plane pod chooser.
37
+ export * from "./cloud";
36
38
 
37
39
  /** A placeholder origin; never contacted. */
38
40
  export const MOCK_POD_URL = "https://mock.synap.invalid";