@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.
- package/README.md +81 -7
- package/dist/{client-CVUT1Mcf.d.cts → client-C5sF20f1.d.cts} +1 -1
- package/dist/{client-CVUT1Mcf.d.ts → client-C5sF20f1.d.ts} +1 -1
- package/dist/connect.cjs +397 -116
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +220 -15
- package/dist/connect.d.ts +220 -15
- package/dist/connect.js +389 -116
- package/dist/connect.js.map +1 -1
- package/dist/public.d.cts +2 -2
- package/dist/public.d.ts +2 -2
- package/dist/testing.cjs +138 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +92 -4
- package/dist/testing.d.ts +92 -4
- package/dist/testing.js +134 -11
- package/dist/testing.js.map +1 -1
- package/dist/{types-DpqPNaaa.d.cts → types-C2KGNXEn.d.cts} +20 -1
- package/dist/{types-DpqPNaaa.d.ts → types-C2KGNXEn.d.ts} +20 -1
- package/package.json +8 -8
- package/src/connect/auth.ts +161 -31
- package/src/connect/client.ts +107 -1
- package/src/connect/cloud.ts +206 -0
- package/src/connect/discovery.ts +15 -14
- package/src/connect/errors.ts +1 -1
- package/src/connect/flow.ts +41 -1
- package/src/connect/index.ts +39 -11
- package/src/connect/seal.ts +1 -1
- package/src/connect/store.ts +41 -3
- package/src/connect/types.ts +20 -0
- package/src/testing/auth-server.ts +144 -12
- package/src/testing/cloud.ts +84 -0
- package/src/testing/index.ts +3 -1
package/dist/testing.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicForm,
|
|
2
|
-
import { P as PodGrant, a as PodUser } from './types-
|
|
1
|
+
import { P as PublicForm, g as PublicShare, e as PublicRecord } from './client-C5sF20f1.cjs';
|
|
2
|
+
import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-C2KGNXEn.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
|
|
@@ -44,6 +44,18 @@ interface MockAuthServerOptions {
|
|
|
44
44
|
refreshIdleSec?: number;
|
|
45
45
|
/** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
|
|
46
46
|
advertiseRefreshExpiry?: boolean;
|
|
47
|
+
/** The reuse grace, seconds (PB3b). Default 30 (the pod's); 0 = off (a pod before PB3b). */
|
|
48
|
+
refreshGraceSec?: number;
|
|
49
|
+
/**
|
|
50
|
+
* `synap_app` on token responses: the connection's id and its page on the
|
|
51
|
+
* pod console. Default `MOCK_APP`; null = a pod that does not send it.
|
|
52
|
+
*/
|
|
53
|
+
app?: {
|
|
54
|
+
id: string;
|
|
55
|
+
manageUrl: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
/** Entities `GET /entities` answers (with `workspaceId` / `profileSlug` filters). */
|
|
58
|
+
entities?: Array<Record<string, unknown>>;
|
|
47
59
|
grant?: PodGrant[];
|
|
48
60
|
user?: PodUser;
|
|
49
61
|
/** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
|
|
@@ -70,6 +82,27 @@ interface AuthorizeDecision {
|
|
|
70
82
|
/** Override `state` on the answer: a forged answer. */
|
|
71
83
|
state?: string;
|
|
72
84
|
}
|
|
85
|
+
/** A proposal a governed write filed (`GET /proposals` lists the pending ones). */
|
|
86
|
+
interface MockProposal {
|
|
87
|
+
id: string;
|
|
88
|
+
proposalType: string;
|
|
89
|
+
targetType: string;
|
|
90
|
+
targetId: string | null;
|
|
91
|
+
status: "pending" | "approved" | "rejected";
|
|
92
|
+
workspaceId: string | null;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
correlationId: string | null;
|
|
95
|
+
sessionId: string | null;
|
|
96
|
+
agentUserId: string | null;
|
|
97
|
+
summary?: string;
|
|
98
|
+
}
|
|
99
|
+
/** The connection's `synap_app` the mock pod answers by default (backend `appTokenResponse`). */
|
|
100
|
+
declare const MOCK_APP: {
|
|
101
|
+
id: string;
|
|
102
|
+
manageUrl: string;
|
|
103
|
+
};
|
|
104
|
+
/** The app's agent on the mock pod: who files its proposals. */
|
|
105
|
+
declare const MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
73
106
|
interface MockAuthServer {
|
|
74
107
|
issuer: string;
|
|
75
108
|
fetch: typeof fetch;
|
|
@@ -80,6 +113,18 @@ interface MockAuthServer {
|
|
|
80
113
|
path: string;
|
|
81
114
|
body: unknown;
|
|
82
115
|
}>;
|
|
116
|
+
/** Proposals filed by proposed writes, in order. */
|
|
117
|
+
proposals: MockProposal[];
|
|
118
|
+
/** The person approved or rejected one: it leaves the pending list. */
|
|
119
|
+
settleProposal(id: string, status: "approved" | "rejected"): void;
|
|
120
|
+
/** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
|
|
121
|
+
redeemElsewhere(token: string): Promise<{
|
|
122
|
+
access_token: string;
|
|
123
|
+
refresh_token: string;
|
|
124
|
+
expires_in: number;
|
|
125
|
+
}>;
|
|
126
|
+
/** Refresh redeems answered within the reuse grace (the same pair again). */
|
|
127
|
+
graceAnswers: number;
|
|
83
128
|
/** Stand-in for the consent screen: validates the request like /authorize, returns where the pod redirects. */
|
|
84
129
|
authorize(authorizeUrl: string, decision?: AuthorizeDecision): string;
|
|
85
130
|
/** Mark a refresh token used elsewhere: presenting it now is a REUSE (family + live key revoked). */
|
|
@@ -96,6 +141,49 @@ interface MockAuthServer {
|
|
|
96
141
|
}
|
|
97
142
|
declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
|
|
98
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`, wave C1)
|
|
146
|
+
* for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
|
|
147
|
+
* `choose()` stands in for the person signing in to the CP and picking a pod.
|
|
148
|
+
*
|
|
149
|
+
* Wire behaviour mirrors the CP route (doc 18 §1.1): it accepts only
|
|
150
|
+
* `client_id` (an https CIMD URL; http on loopback only with
|
|
151
|
+
* `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
|
|
152
|
+
* `state` (`^[A-Za-z0-9._~-]{16,256}$`),
|
|
153
|
+
* takes the return URL from the app's client metadata (`initiate_login_uri`),
|
|
154
|
+
* never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
|
|
155
|
+
* It never sees a code or a token.
|
|
156
|
+
*
|
|
157
|
+
* const cloud = createMockCloud();
|
|
158
|
+
* createPodAppAuth({ ...config, cloudUrl: cloud.url });
|
|
159
|
+
* cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/** A placeholder Control Plane origin; never contacted. */
|
|
163
|
+
declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
164
|
+
interface MockCloud {
|
|
165
|
+
url: string;
|
|
166
|
+
/** The chooser URLs the app sent browsers to. */
|
|
167
|
+
requests: string[];
|
|
168
|
+
/**
|
|
169
|
+
* The person picks `pod` on the chooser: validates the request like the CP,
|
|
170
|
+
* returns where the CP redirects the browser. Throws on a request the CP
|
|
171
|
+
* refuses (shown as a page there, never redirected).
|
|
172
|
+
*/
|
|
173
|
+
choose(location: string, opts: {
|
|
174
|
+
pod: string;
|
|
175
|
+
metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
|
|
176
|
+
}): string;
|
|
177
|
+
/** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
|
|
178
|
+
cancel(location: string, opts: {
|
|
179
|
+
metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
|
|
180
|
+
}): string;
|
|
181
|
+
}
|
|
182
|
+
declare function createMockCloud(options?: {
|
|
183
|
+
url?: string;
|
|
184
|
+
allowInsecureLoopback?: boolean;
|
|
185
|
+
}): MockCloud;
|
|
186
|
+
|
|
99
187
|
/**
|
|
100
188
|
* @synap-core/sdk/testing — a mock pod: a fake `fetch` that answers the pod's
|
|
101
189
|
* public doors (forms, shares, sites) from fixtures, with no network. Inject
|
|
@@ -115,7 +203,7 @@ declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuth
|
|
|
115
203
|
* anything else the uniform 404
|
|
116
204
|
*
|
|
117
205
|
* Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
|
|
118
|
-
* ./auth-server.ts.
|
|
206
|
+
* ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
119
207
|
*/
|
|
120
208
|
|
|
121
209
|
/** A placeholder origin; never contacted. */
|
|
@@ -178,4 +266,4 @@ interface MockPod {
|
|
|
178
266
|
}
|
|
179
267
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
180
268
|
|
|
181
|
-
export { type AuthorizeDecision, MOCK_AUTH_ISSUER, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockPod };
|
|
269
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockProposal, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicForm,
|
|
2
|
-
import { P as PodGrant, a as PodUser } from './types-
|
|
1
|
+
import { P as PublicForm, g as PublicShare, e as PublicRecord } from './client-C5sF20f1.js';
|
|
2
|
+
import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-C2KGNXEn.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
|
|
@@ -44,6 +44,18 @@ interface MockAuthServerOptions {
|
|
|
44
44
|
refreshIdleSec?: number;
|
|
45
45
|
/** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
|
|
46
46
|
advertiseRefreshExpiry?: boolean;
|
|
47
|
+
/** The reuse grace, seconds (PB3b). Default 30 (the pod's); 0 = off (a pod before PB3b). */
|
|
48
|
+
refreshGraceSec?: number;
|
|
49
|
+
/**
|
|
50
|
+
* `synap_app` on token responses: the connection's id and its page on the
|
|
51
|
+
* pod console. Default `MOCK_APP`; null = a pod that does not send it.
|
|
52
|
+
*/
|
|
53
|
+
app?: {
|
|
54
|
+
id: string;
|
|
55
|
+
manageUrl: string | null;
|
|
56
|
+
} | null;
|
|
57
|
+
/** Entities `GET /entities` answers (with `workspaceId` / `profileSlug` filters). */
|
|
58
|
+
entities?: Array<Record<string, unknown>>;
|
|
47
59
|
grant?: PodGrant[];
|
|
48
60
|
user?: PodUser;
|
|
49
61
|
/** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
|
|
@@ -70,6 +82,27 @@ interface AuthorizeDecision {
|
|
|
70
82
|
/** Override `state` on the answer: a forged answer. */
|
|
71
83
|
state?: string;
|
|
72
84
|
}
|
|
85
|
+
/** A proposal a governed write filed (`GET /proposals` lists the pending ones). */
|
|
86
|
+
interface MockProposal {
|
|
87
|
+
id: string;
|
|
88
|
+
proposalType: string;
|
|
89
|
+
targetType: string;
|
|
90
|
+
targetId: string | null;
|
|
91
|
+
status: "pending" | "approved" | "rejected";
|
|
92
|
+
workspaceId: string | null;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
correlationId: string | null;
|
|
95
|
+
sessionId: string | null;
|
|
96
|
+
agentUserId: string | null;
|
|
97
|
+
summary?: string;
|
|
98
|
+
}
|
|
99
|
+
/** The connection's `synap_app` the mock pod answers by default (backend `appTokenResponse`). */
|
|
100
|
+
declare const MOCK_APP: {
|
|
101
|
+
id: string;
|
|
102
|
+
manageUrl: string;
|
|
103
|
+
};
|
|
104
|
+
/** The app's agent on the mock pod: who files its proposals. */
|
|
105
|
+
declare const MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
73
106
|
interface MockAuthServer {
|
|
74
107
|
issuer: string;
|
|
75
108
|
fetch: typeof fetch;
|
|
@@ -80,6 +113,18 @@ interface MockAuthServer {
|
|
|
80
113
|
path: string;
|
|
81
114
|
body: unknown;
|
|
82
115
|
}>;
|
|
116
|
+
/** Proposals filed by proposed writes, in order. */
|
|
117
|
+
proposals: MockProposal[];
|
|
118
|
+
/** The person approved or rejected one: it leaves the pending list. */
|
|
119
|
+
settleProposal(id: string, status: "approved" | "rejected"): void;
|
|
120
|
+
/** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
|
|
121
|
+
redeemElsewhere(token: string): Promise<{
|
|
122
|
+
access_token: string;
|
|
123
|
+
refresh_token: string;
|
|
124
|
+
expires_in: number;
|
|
125
|
+
}>;
|
|
126
|
+
/** Refresh redeems answered within the reuse grace (the same pair again). */
|
|
127
|
+
graceAnswers: number;
|
|
83
128
|
/** Stand-in for the consent screen: validates the request like /authorize, returns where the pod redirects. */
|
|
84
129
|
authorize(authorizeUrl: string, decision?: AuthorizeDecision): string;
|
|
85
130
|
/** Mark a refresh token used elsewhere: presenting it now is a REUSE (family + live key revoked). */
|
|
@@ -96,6 +141,49 @@ interface MockAuthServer {
|
|
|
96
141
|
}
|
|
97
142
|
declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
|
|
98
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`, wave C1)
|
|
146
|
+
* for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
|
|
147
|
+
* `choose()` stands in for the person signing in to the CP and picking a pod.
|
|
148
|
+
*
|
|
149
|
+
* Wire behaviour mirrors the CP route (doc 18 §1.1): it accepts only
|
|
150
|
+
* `client_id` (an https CIMD URL; http on loopback only with
|
|
151
|
+
* `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
|
|
152
|
+
* `state` (`^[A-Za-z0-9._~-]{16,256}$`),
|
|
153
|
+
* takes the return URL from the app's client metadata (`initiate_login_uri`),
|
|
154
|
+
* never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
|
|
155
|
+
* It never sees a code or a token.
|
|
156
|
+
*
|
|
157
|
+
* const cloud = createMockCloud();
|
|
158
|
+
* createPodAppAuth({ ...config, cloudUrl: cloud.url });
|
|
159
|
+
* cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/** A placeholder Control Plane origin; never contacted. */
|
|
163
|
+
declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
164
|
+
interface MockCloud {
|
|
165
|
+
url: string;
|
|
166
|
+
/** The chooser URLs the app sent browsers to. */
|
|
167
|
+
requests: string[];
|
|
168
|
+
/**
|
|
169
|
+
* The person picks `pod` on the chooser: validates the request like the CP,
|
|
170
|
+
* returns where the CP redirects the browser. Throws on a request the CP
|
|
171
|
+
* refuses (shown as a page there, never redirected).
|
|
172
|
+
*/
|
|
173
|
+
choose(location: string, opts: {
|
|
174
|
+
pod: string;
|
|
175
|
+
metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
|
|
176
|
+
}): string;
|
|
177
|
+
/** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
|
|
178
|
+
cancel(location: string, opts: {
|
|
179
|
+
metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
|
|
180
|
+
}): string;
|
|
181
|
+
}
|
|
182
|
+
declare function createMockCloud(options?: {
|
|
183
|
+
url?: string;
|
|
184
|
+
allowInsecureLoopback?: boolean;
|
|
185
|
+
}): MockCloud;
|
|
186
|
+
|
|
99
187
|
/**
|
|
100
188
|
* @synap-core/sdk/testing — a mock pod: a fake `fetch` that answers the pod's
|
|
101
189
|
* public doors (forms, shares, sites) from fixtures, with no network. Inject
|
|
@@ -115,7 +203,7 @@ declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuth
|
|
|
115
203
|
* anything else the uniform 404
|
|
116
204
|
*
|
|
117
205
|
* Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
|
|
118
|
-
* ./auth-server.ts.
|
|
206
|
+
* ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
119
207
|
*/
|
|
120
208
|
|
|
121
209
|
/** A placeholder origin; never contacted. */
|
|
@@ -178,4 +266,4 @@ interface MockPod {
|
|
|
178
266
|
}
|
|
179
267
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
180
268
|
|
|
181
|
-
export { type AuthorizeDecision, MOCK_AUTH_ISSUER, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockPod };
|
|
269
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockProposal, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod };
|
package/dist/testing.js
CHANGED
|
@@ -105,6 +105,8 @@ var MOCK_GRANT = [
|
|
|
105
105
|
{ permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
|
|
106
106
|
];
|
|
107
107
|
var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
|
|
108
|
+
var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
|
|
109
|
+
var MOCK_APP_AGENT_ID = "agent_mock_app";
|
|
108
110
|
function json(status, body, headers = {}) {
|
|
109
111
|
return new Response(JSON.stringify(body), {
|
|
110
112
|
status,
|
|
@@ -127,7 +129,12 @@ function createMockAuthServer(options = {}) {
|
|
|
127
129
|
const refreshTokens = /* @__PURE__ */ new Map();
|
|
128
130
|
const requests = [];
|
|
129
131
|
const writes = [];
|
|
132
|
+
const proposals = [];
|
|
133
|
+
const entities = [...options.entities ?? []];
|
|
134
|
+
const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
|
|
135
|
+
const app = options.app === void 0 ? MOCK_APP : options.app;
|
|
130
136
|
let proposalSeq = 0;
|
|
137
|
+
let graceAnswers = 0;
|
|
131
138
|
const metadata = () => {
|
|
132
139
|
const base = {
|
|
133
140
|
issuer,
|
|
@@ -155,18 +162,24 @@ function createMockAuthServer(options = {}) {
|
|
|
155
162
|
for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
|
|
156
163
|
for (const a of access.values()) if (a.family === family) a.revoked = true;
|
|
157
164
|
};
|
|
158
|
-
const tokenBody = (scope, clientId, family) => {
|
|
159
|
-
const accessToken = `synap_hub_mock_${randomToken()}`;
|
|
160
|
-
access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null });
|
|
165
|
+
const tokenBody = (scope, clientId, family, pair) => {
|
|
166
|
+
const accessToken = pair?.accessToken ?? `synap_hub_mock_${randomToken()}`;
|
|
167
|
+
if (!pair) access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null, scope });
|
|
168
|
+
const row = access.get(accessToken);
|
|
161
169
|
const body = {
|
|
162
170
|
access_token: accessToken,
|
|
163
171
|
token_type: "Bearer",
|
|
164
|
-
|
|
172
|
+
// As the pod: what the key has left, never an assumed hour.
|
|
173
|
+
expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
|
|
165
174
|
scope: scope.join(" "),
|
|
166
175
|
synap_grant: grant.filter((g) => scope.includes(g.permission)),
|
|
167
|
-
synap_user: user
|
|
176
|
+
synap_user: user,
|
|
177
|
+
...app ? { synap_app: app } : {}
|
|
168
178
|
};
|
|
169
|
-
if (
|
|
179
|
+
if (pair) {
|
|
180
|
+
body.refresh_token = pair.refreshToken;
|
|
181
|
+
if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = Math.floor((refreshTokens.get(pair.refreshToken).idleUntil - now()) / 1e3);
|
|
182
|
+
} else if (refresh) {
|
|
170
183
|
const refreshToken = randomToken();
|
|
171
184
|
refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
|
|
172
185
|
body.refresh_token = refreshToken;
|
|
@@ -198,6 +211,13 @@ function createMockAuthServer(options = {}) {
|
|
|
198
211
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
199
212
|
}
|
|
200
213
|
if (row.used) {
|
|
214
|
+
const successor = row.successor ? refreshTokens.get(row.successor) : void 0;
|
|
215
|
+
const key = row.pairAccess ? access.get(row.pairAccess) : void 0;
|
|
216
|
+
const inGrace = graceMs > 0 && row.usedAt !== void 0 && now() - row.usedAt < graceMs;
|
|
217
|
+
if (inGrace && successor && !successor.used && !successor.revoked && key && !key.revoked && key.expiresAt > now()) {
|
|
218
|
+
graceAnswers++;
|
|
219
|
+
return json(200, tokenBody(row.scope, row.clientId, row.family, { accessToken: row.pairAccess, refreshToken: row.successor }));
|
|
220
|
+
}
|
|
201
221
|
reuses++;
|
|
202
222
|
revokeFamily(row.family);
|
|
203
223
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
@@ -208,11 +228,16 @@ function createMockAuthServer(options = {}) {
|
|
|
208
228
|
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
209
229
|
}
|
|
210
230
|
row.used = true;
|
|
211
|
-
|
|
231
|
+
row.usedAt = now();
|
|
232
|
+
for (const a of access.values()) if (a.family === row.family) a.revoked = true;
|
|
233
|
+
const body = tokenBody(row.scope, row.clientId, row.family);
|
|
234
|
+
row.successor = body.refresh_token;
|
|
235
|
+
row.pairAccess = body.access_token;
|
|
236
|
+
return json(200, body);
|
|
212
237
|
}
|
|
213
238
|
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
214
239
|
}
|
|
215
|
-
function hub(method, path, headers, body) {
|
|
240
|
+
function hub(method, path, query, headers, body) {
|
|
216
241
|
const failure2 = options.failures?.[path];
|
|
217
242
|
if (failure2 === "unreachable") return "reject";
|
|
218
243
|
const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
|
|
@@ -234,11 +259,43 @@ function createMockAuthServer(options = {}) {
|
|
|
234
259
|
return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
|
|
235
260
|
}
|
|
236
261
|
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
237
|
-
if (method === "GET" && path === "/entities")
|
|
262
|
+
if (method === "GET" && path === "/entities") {
|
|
263
|
+
const ws = query.get("workspaceId");
|
|
264
|
+
const kind = query.get("profileSlug");
|
|
265
|
+
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
266
|
+
const limit = Number(query.get("limit") ?? 50) || 50;
|
|
267
|
+
const rows = entities.filter((e) => (!ws || e.workspaceId === ws) && (!kind || e.profileSlug === kind));
|
|
268
|
+
return json(200, rows.slice(offset, offset + limit));
|
|
269
|
+
}
|
|
270
|
+
if (method === "GET" && path === "/proposals") {
|
|
271
|
+
const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
|
|
272
|
+
const status = query.get("status") ?? "pending";
|
|
273
|
+
const ws = query.get("workspaceId");
|
|
274
|
+
const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
|
|
275
|
+
const offset = Number(query.get("offset") ?? 0) || 0;
|
|
276
|
+
const rows = proposals.filter((p) => p.workspaceId !== null && readable.has(p.workspaceId)).filter((p) => (status === "all" || p.status === status) && (!ws || p.workspaceId === ws)).reverse();
|
|
277
|
+
const page = rows.slice(offset, offset + limit);
|
|
278
|
+
return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
|
|
279
|
+
}
|
|
238
280
|
if (method !== "GET") {
|
|
239
281
|
writes.push({ method, path, body });
|
|
282
|
+
const fields = body && typeof body === "object" ? body : {};
|
|
283
|
+
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
240
284
|
if ((options.writeMode ?? "proposed") === "proposed") {
|
|
241
285
|
const proposalId = `prop_mock_${++proposalSeq}`;
|
|
286
|
+
proposals.push({
|
|
287
|
+
id: proposalId,
|
|
288
|
+
proposalType: method === "POST" ? "create" : method === "DELETE" ? "delete" : "update",
|
|
289
|
+
targetType: path.split("/")[1]?.replace(/s$/, "") ?? "entity",
|
|
290
|
+
targetId: null,
|
|
291
|
+
status: "pending",
|
|
292
|
+
workspaceId,
|
|
293
|
+
createdAt: new Date(now()).toISOString(),
|
|
294
|
+
correlationId: null,
|
|
295
|
+
sessionId: null,
|
|
296
|
+
agentUserId: MOCK_APP_AGENT_ID,
|
|
297
|
+
...typeof fields.title === "string" ? { summary: `Create "${fields.title}"` } : {}
|
|
298
|
+
});
|
|
242
299
|
return json(200, {
|
|
243
300
|
status: "proposed",
|
|
244
301
|
message: "Sent for review",
|
|
@@ -246,7 +303,9 @@ function createMockAuthServer(options = {}) {
|
|
|
246
303
|
reviewUrl: `${issuer}/open/${proposalId}`
|
|
247
304
|
});
|
|
248
305
|
}
|
|
249
|
-
|
|
306
|
+
const id = `ent_mock_${writes.length}`;
|
|
307
|
+
if (method === "POST" && path === "/entities") entities.push({ id, ...fields });
|
|
308
|
+
return json(200, { status: "created", id });
|
|
250
309
|
}
|
|
251
310
|
return json(404, { error: "Not found" });
|
|
252
311
|
}
|
|
@@ -285,7 +344,7 @@ function createMockAuthServer(options = {}) {
|
|
|
285
344
|
} catch {
|
|
286
345
|
body = rawBody;
|
|
287
346
|
}
|
|
288
|
-
const res = hub(method, hubPath, headers, body);
|
|
347
|
+
const res = hub(method, hubPath, url.searchParams, headers, body);
|
|
289
348
|
if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
|
|
290
349
|
return res;
|
|
291
350
|
}
|
|
@@ -296,6 +355,20 @@ function createMockAuthServer(options = {}) {
|
|
|
296
355
|
fetch: mockFetch,
|
|
297
356
|
requests,
|
|
298
357
|
writes,
|
|
358
|
+
proposals,
|
|
359
|
+
settleProposal(id, status) {
|
|
360
|
+
const p = proposals.find((x) => x.id === id);
|
|
361
|
+
if (p) p.status = status;
|
|
362
|
+
},
|
|
363
|
+
async redeemElsewhere(t) {
|
|
364
|
+
const row = refreshTokens.get(t);
|
|
365
|
+
const res = await token({ grant_type: "refresh_token", refresh_token: t, client_id: row?.clientId ?? "" });
|
|
366
|
+
if (res.status !== 200) throw new Error(`redeemElsewhere: the pod answered ${res.status}`);
|
|
367
|
+
return await res.json();
|
|
368
|
+
},
|
|
369
|
+
get graceAnswers() {
|
|
370
|
+
return graceAnswers;
|
|
371
|
+
},
|
|
299
372
|
authorize(authorizeUrl, decision = {}) {
|
|
300
373
|
const url = new URL(authorizeUrl);
|
|
301
374
|
if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
|
|
@@ -351,6 +424,52 @@ function createMockAuthServer(options = {}) {
|
|
|
351
424
|
};
|
|
352
425
|
}
|
|
353
426
|
|
|
427
|
+
// src/testing/cloud.ts
|
|
428
|
+
var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
429
|
+
var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
430
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
431
|
+
function createMockCloud(options = {}) {
|
|
432
|
+
const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
|
|
433
|
+
const requests = [];
|
|
434
|
+
return {
|
|
435
|
+
url: base,
|
|
436
|
+
requests,
|
|
437
|
+
choose(location, { pod, metadata }) {
|
|
438
|
+
const back = check(location, metadata);
|
|
439
|
+
back.searchParams.set("iss", pod);
|
|
440
|
+
back.searchParams.set("state", new URL(location).searchParams.get("state"));
|
|
441
|
+
return back.href;
|
|
442
|
+
},
|
|
443
|
+
cancel(location, { metadata }) {
|
|
444
|
+
const back = check(location, metadata);
|
|
445
|
+
back.searchParams.set("state", new URL(location).searchParams.get("state"));
|
|
446
|
+
back.searchParams.set("error", "access_denied");
|
|
447
|
+
return back.href;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
function check(location, metadata) {
|
|
451
|
+
requests.push(location);
|
|
452
|
+
const url = new URL(location);
|
|
453
|
+
if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
|
|
454
|
+
const clientId = url.searchParams.get("client_id");
|
|
455
|
+
const state = url.searchParams.get("state");
|
|
456
|
+
if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
|
|
457
|
+
if (!state || !STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
|
|
458
|
+
if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
|
|
459
|
+
return new URL(metadata.initiate_login_uri);
|
|
460
|
+
}
|
|
461
|
+
function cimdUrlAccepted(clientId) {
|
|
462
|
+
let url;
|
|
463
|
+
try {
|
|
464
|
+
url = new URL(clientId);
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
if (url.protocol === "https:") return true;
|
|
469
|
+
return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
354
473
|
// src/testing/index.ts
|
|
355
474
|
var MOCK_POD_URL = "https://mock.synap.invalid";
|
|
356
475
|
var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
|
|
@@ -598,7 +717,10 @@ function createMockPod(options = {}) {
|
|
|
598
717
|
};
|
|
599
718
|
}
|
|
600
719
|
export {
|
|
720
|
+
MOCK_APP,
|
|
721
|
+
MOCK_APP_AGENT_ID,
|
|
601
722
|
MOCK_AUTH_ISSUER,
|
|
723
|
+
MOCK_CLOUD_URL,
|
|
602
724
|
MOCK_FAILURE_TOKENS,
|
|
603
725
|
MOCK_FORM,
|
|
604
726
|
MOCK_GRANT,
|
|
@@ -609,6 +731,7 @@ export {
|
|
|
609
731
|
MOCK_TOKENS,
|
|
610
732
|
MOCK_USER,
|
|
611
733
|
createMockAuthServer,
|
|
734
|
+
createMockCloud,
|
|
612
735
|
createMockPod
|
|
613
736
|
};
|
|
614
737
|
//# sourceMappingURL=testing.js.map
|