@synap-core/sdk 1.2.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 CHANGED
@@ -119,6 +119,160 @@ SDK refuses out-of-bounds `offset`/`limit`/`sort` locally, without a request),
119
119
  pass `ifNoneMatch` from a browser: the doors allow no cross-origin request header
120
120
  but `Content-Type`, so the preflight fails (browsers revalidate on their own).
121
121
 
122
+ ## Pod-app sign-in (server-only): `@synap-core/sdk/connect` (1.3.0, 1.4.0)
123
+
124
+ An app that reads and writes a person's OWN pod connects as an OAuth 2.1 public
125
+ client of that pod: its `client_id` is the https URL of its client metadata
126
+ document (CIMD), PKCE S256, every answer checked for the pod's `iss`. The pod
127
+ token stays on the app's server, sealed (AES-GCM) in an HttpOnly cookie; the
128
+ browser never sees it. Framework-free and Web-standard (`Request` → `Response`,
129
+ WebCrypto): Next route handlers and `proxy.ts`, Hono, Remix, edge runtimes.
130
+ **Server-only**: it throws when loaded in a browser, and a browser bundle
131
+ resolves it to a stub that throws.
132
+
133
+ ```typescript
134
+ // lib/synap/pod-app/auth.ts (server-only)
135
+ import { createPodAppAuth } from "@synap-core/sdk/connect";
136
+
137
+ export const synap = createPodAppAuth({
138
+ appUrl: process.env.SITE_URL!, // https://studio.example.com
139
+ clientName: "Studio",
140
+ requests: [
141
+ { permission: "entity.post.read", reason: "Show your posts" },
142
+ { permission: "entity.post.create", reason: "Save drafts to your pod" },
143
+ ],
144
+ secret: process.env.SYNAP_APP_SESSION_SECRET!, // ≥ 32 chars, never NEXT_PUBLIC_
145
+ cloudUrl: process.env.SYNAP_CLOUD_URL, // optional (1.4): https://api.synap.live offers "Continue with Synap Cloud"
146
+ });
147
+
148
+ // app/.well-known/synap-client.json/route.ts export const GET = synap.handlers.clientMetadata;
149
+ // app/api/synap/connect/route.ts export const POST = synap.handlers.connect; // form { pod, next?, space?, prompt? }
150
+ // app/api/synap/callback/route.ts export const GET = synap.handlers.callback;
151
+ // app/api/synap/disconnect/route.ts export const POST = synap.handlers.disconnect; // form { next? }
152
+ // app/api/synap/cloud/route.ts (1.4) export const POST = synap.handlers.cloudConnect; // form { next?, space?, prompt? }
153
+ // export const GET = synap.handlers.cloudReturn;
154
+ ```
155
+
156
+ ```typescript
157
+ // proxy.ts: refresh before render (server components cannot set cookies)
158
+ const out = await synap.refreshIfNeeded(request);
159
+ // out.status: none | skipped | fresh | refreshed | kept | expired | revoked | unavailable
160
+ // out.lapsed (with none, 1.4): { podUrl } when this browser WAS connected and the connection lapsed
161
+ // send out.setCookies on the response; forward out.cookieHeader (when set) as the request's Cookie
162
+
163
+ // Next 16 strips its router headers before proxy code runs, so skip prefetches in the MATCHER
164
+ // (it still sees them; matcher values must be literals):
165
+ export const config = {
166
+ matcher: [{
167
+ source: "/((?!_next/static|_next/image|api/synap/|\\.well-known/|.*\\.\\w+$).*)",
168
+ missing: [{ type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" }],
169
+ }],
170
+ };
171
+
172
+ // a server component / action (no Request there: pass the Cookie header)
173
+ const connection = await synap.readConnection((await headers()).get("cookie"));
174
+ const pod = synap.client(connection!);
175
+ const res = await pod.post("/entities", { profileSlug: "post", title: "Hello" });
176
+ if (res.ok && res.status === "proposed") showPending(res.reviewUrl); // success: queued for review, never retry
177
+ if (!res.ok) res.error.code; // expired | unauthorized | forbidden | door_unsupported | rate_limited (retryAfterSec) | pod_unreachable | …
178
+ ```
179
+
180
+ - **Discovery**: `discoverPod("pod.example.com")` fetches the pod's RFC 8414
181
+ metadata and requires `issuer` equal to the address, CIMD, `iss` on answers and
182
+ S256 (`not_a_pod`, `pod_unsupported`, `pod_unreachable`). A browser launch
183
+ (`?iss=…&target_link_uri=…`) is read with `acceptLaunch` and only pre-fills the
184
+ connect page.
185
+ - **Failures** redirect with `?error=<ConnectErrorCode>`: `denied`,
186
+ `expired_transaction`, `mismatch`, `misconfigured`, `rate_limited`,
187
+ `pod_unreachable`, … (`parseConnectErrorCode` reads it back). `connect` and
188
+ `disconnect` are POST and same-origin only.
189
+ - **Refresh** (rotating, single-use, 30 days from issue) is feature-detected: a
190
+ pod that issues no `refresh_token` gives a connection that ends with its
191
+ 1-hour access token. `refreshIfNeeded` rotates only inside the last 5
192
+ minutes, is single-flight per connection in the process, and persists the
193
+ rotated connection BEFORE returning it. The pod revokes the whole connection
194
+ when a spent refresh token is presented again, so for 2 minutes a request
195
+ still carrying the spent token (a parallel request, a second tab) gets the
196
+ rotation's result instead of reaching the pod. Across server instances only
197
+ a shared server-side `ConnectionStore` rules that replay out. A concurrent
198
+ loser (refused softly by the pod) keeps the still-valid access token.
199
+ The pod's reuse grace (PB3b) answers a spent token presented again within
200
+ 30 s with the same pair the first use got; its `expires_in` may be under an
201
+ hour, and the SDK reads it as sent.
202
+ - **Your own store**: implement `ConnectionStore` (`read`, `write`, `clear`,
203
+ returning Set-Cookie values) and pass it as `store`.
204
+ - Lower level: `startConnect`, `handleCallback`, `refreshConnection`,
205
+ `revokeConnection`, `buildClientMetadata`, `seal`/`unseal`,
206
+ `startCloudConnect`, `acceptCloudReturn`.
207
+
208
+ ### 1.4.0 (additive)
209
+
210
+ - **Re-consent**: the connect form's `prompt=consent` (or `startConnect({ prompt: "consent" })`)
211
+ goes to the pod's consent screen even when this browser is already connected to
212
+ that pod (to widen or narrow access). It is a normal sign-in: fresh PKCE verifier
213
+ and state, `iss` checked; a denied or forged answer leaves the live connection as it is.
214
+ - **Synap Cloud** (wave C1): with `cloudUrl` set, `synap.cloudConnectAvailable` is
215
+ true and `cloudConnectAvailable({ cloudUrl })` too. POST `handlers.cloudConnect`
216
+ sends the browser to `<cp>/connect/pod?client_id=<client id>&state=<nonce>`, the
217
+ state sealed in an HttpOnly cookie of that browser (10 minutes). The CP returns to
218
+ the CIMD `initiate_login_uri?iss=<pod>&state=<nonce>` (your connect page):
219
+ `acceptLaunch(url).cloudState` is set, so forward the request's query to GET
220
+ `handlers.cloudReturn`, which checks the state against the cookie (constant time,
221
+ single use) BEFORE the pod is used, then runs the same sign-in as a typed address.
222
+ `acceptLaunch` itself never verifies anything; a return without a matching state
223
+ starts no sign-in (`mismatch`, `expired_transaction`). A cancel on the chooser
224
+ (`?state=…&error=access_denied`, no `iss`) also carries `cloudState`: forwarded the
225
+ same way, it lands on the connect page with `?error=denied` once the state matched.
226
+ Security notes: after the state matches, the named pod goes through the same
227
+ discovery as a typed address (metadata, `issuer` equality, CIMD, `iss`, S256)
228
+ before the browser is sent anywhere. The cloud cookie is single-use and cleared on
229
+ every outcome (success, cancel, mismatch). Accepted trade-off: a third-party page
230
+ that sends a visitor's browser to a forged return can cancel that visitor's Cloud
231
+ sign-in in progress (they click again); nothing leaks and nothing is bound, the
232
+ same nuisance any OAuth `state` check allows.
233
+
234
+ ```typescript
235
+ // app/connect/page.tsx (server component)
236
+ const launch = acceptLaunch(url, origin);
237
+ if (launch.cloudState) redirect(`${synap.paths.cloud}${new URL(url).search}`);
238
+ ```
239
+ - **`connection.manageUrl` / `connection.appId`**: the pod's page for this app
240
+ connection (pod-admin `/apps/<appId>`), read from the token response's
241
+ `synap_app: { id, manageUrl }` (https only). Absent when the pod does not send it:
242
+ hide the link, never build one.
243
+ - **`client.listPendingProposals({ workspaceId?, limit?, offset? })`**: "Waiting for
244
+ review: N" (`data.total`, plus summary rows with `reviewUrl`) from the pod's seamed
245
+ `GET /proposals` door. Ask for `proposal.read` in `requests`: without it the answer
246
+ is `forbidden` and nothing is sent (the pod would answer an empty list). It counts
247
+ the person's pending proposals in those spaces, from any agent.
248
+ - **`spaces(connection)` / `client.spaces()`**: the grant by space
249
+ (`{ workspaceId, workspaceName, permissions }`), each permission once.
250
+ - **Expired vs never connected**: each sign-in and refresh also sets a marker cookie
251
+ (`__Host-synap_seen`, 400 days) holding the pod's URL and nothing else.
252
+ `synap.readLapsed(cookieHeader)` and `RefreshOutcome.lapsed` say "this browser was
253
+ connected to <pod>" once the sealed session is gone; a disconnect clears it.
254
+ - **`isPrefetchOrAsset(request)`** is exported, and its limit documented: inside
255
+ Next 16's proxy a router prefetch is invisible (see the matcher above).
256
+ - Not added: `ensureTemplate` on the pod-app client. A pod-app key's grant is pinned
257
+ to the approved spaces, so creating a space is refused by the pod's grant check
258
+ every time; ask for templates with `templates` (CIMD `synap_templates`) instead:
259
+ the pod's consent offers to create the space.
260
+
261
+ Test without a pod: `createMockAuthServer()` from `@synap-core/sdk/testing`
262
+ (metadata, `authorize()` as the consent screen, `/token` with PKCE, rotating
263
+ refresh with the 30 s reuse grace, `/revoke`, Hub REST doors: `GET /entities`
264
+ answers an array as the pod does, `GET /proposals` lists what proposed writes
265
+ filed; `synap_app` is `MOCK_APP` unless `app: null`; `writeMode`, `failures`, `refresh: false`, `entities`,
266
+ `refreshGraceSec`), and `createMockCloud()` for the Synap Cloud pod chooser:
267
+
268
+ ```typescript
269
+ const pod = createMockAuthServer();
270
+ const cloud = createMockCloud();
271
+ const synap = createPodAppAuth({ ...config, fetch: pod.fetch, cloudUrl: cloud.url });
272
+ // POST connect → Location → pod.authorize(location) → GET callback with that URL
273
+ // POST cloud → Location → cloud.choose(location, { pod: pod.issuer, metadata: synap.metadata }) → forward to GET cloud
274
+ ```
275
+
122
276
  ## Mock pod: `@synap-core/sdk/testing`
123
277
 
124
278
  A fake `fetch` that answers the public doors (forms, shares, sites) from fixtures,
@@ -0,0 +1,79 @@
1
+ // src/connect/encoding.ts
2
+ var encoder = new TextEncoder();
3
+ var decoder = new TextDecoder();
4
+ function utf8(text) {
5
+ return encoder.encode(text);
6
+ }
7
+ function fromUtf8(bytes) {
8
+ return decoder.decode(bytes);
9
+ }
10
+ function base64url(bytes) {
11
+ let binary = "";
12
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
13
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
14
+ }
15
+ function fromBase64url(text) {
16
+ if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;
17
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((text.length + 3) % 4);
18
+ let binary;
19
+ try {
20
+ binary = atob(padded);
21
+ } catch {
22
+ return null;
23
+ }
24
+ const out = new Uint8Array(binary.length);
25
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
26
+ return out;
27
+ }
28
+ function randomBytes(length) {
29
+ const out = new Uint8Array(length);
30
+ crypto.getRandomValues(out);
31
+ return out;
32
+ }
33
+ function randomToken() {
34
+ return base64url(randomBytes(32));
35
+ }
36
+ async function pkceChallenge(verifier) {
37
+ const digest = await crypto.subtle.digest("SHA-256", utf8(verifier));
38
+ return base64url(new Uint8Array(digest));
39
+ }
40
+ function constantTimeEqual(a, b) {
41
+ const x = utf8(a);
42
+ const y = utf8(b);
43
+ let diff = x.length ^ y.length;
44
+ for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
45
+ return diff === 0;
46
+ }
47
+ function parseCookies(header) {
48
+ const out = /* @__PURE__ */ new Map();
49
+ if (!header) return out;
50
+ for (const part of header.split(";")) {
51
+ const eq = part.indexOf("=");
52
+ if (eq < 0) continue;
53
+ const name = part.slice(0, eq).trim();
54
+ if (!name || out.has(name)) continue;
55
+ let value = part.slice(eq + 1).trim();
56
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
57
+ out.set(name, value);
58
+ }
59
+ return out;
60
+ }
61
+ function serializeCookie(name, value, attrs) {
62
+ const parts = [`${name}=${value}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${Math.max(0, Math.floor(attrs.maxAgeSec))}`];
63
+ if (attrs.secure) parts.push("Secure");
64
+ return parts.join("; ");
65
+ }
66
+
67
+ export {
68
+ utf8,
69
+ fromUtf8,
70
+ base64url,
71
+ fromBase64url,
72
+ randomBytes,
73
+ randomToken,
74
+ pkceChallenge,
75
+ constantTimeEqual,
76
+ parseCookies,
77
+ serializeCookie
78
+ };
79
+ //# sourceMappingURL=chunk-J7XHAAFJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/encoding.ts"],"sourcesContent":["/**\n * Web-standard primitives (WebCrypto, TextEncoder, btoa/atob): the same code\n * runs on Node 18+, edge runtimes and workers. No Node import.\n */\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/** `Uint8Array<ArrayBuffer>`: what WebCrypto's `BufferSource` takes under TS 5.7+ lib types. */\nexport type Bytes = Uint8Array<ArrayBuffer>;\n\nexport function utf8(text: string): Bytes {\n return encoder.encode(text);\n}\n\nexport function fromUtf8(bytes: Uint8Array): string {\n return decoder.decode(bytes);\n}\n\nexport function base64url(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/** Null when `text` is not base64url. */\nexport function fromBase64url(text: string): Bytes | null {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;\n const padded = text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"===\".slice((text.length + 3) % 4);\n let binary: string;\n try {\n binary = atob(padded);\n } catch {\n return null;\n }\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nexport function randomBytes(length: number): Bytes {\n const out = new Uint8Array(length);\n crypto.getRandomValues(out);\n return out;\n}\n\n/** 32 random bytes, base64url (43 chars): a state value or a PKCE verifier. */\nexport function randomToken(): string {\n return base64url(randomBytes(32));\n}\n\n/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */\nexport async function pkceChallenge(verifier: string): Promise<string> {\n const digest = await crypto.subtle.digest(\"SHA-256\", utf8(verifier));\n return base64url(new Uint8Array(digest));\n}\n\n/** Compare two strings without an early exit on the first differing byte. */\nexport function constantTimeEqual(a: string, b: string): boolean {\n const x = utf8(a);\n const y = utf8(b);\n let diff = x.length ^ y.length;\n for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);\n return diff === 0;\n}\n\n/** The request's cookies by name (the first value wins, as browsers order the most specific first). */\nexport function parseCookies(header: string | null): Map<string, string> {\n const out = new Map<string, string>();\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const name = part.slice(0, eq).trim();\n if (!name || out.has(name)) continue;\n let value = part.slice(eq + 1).trim();\n if (value.startsWith('\"') && value.endsWith('\"')) value = value.slice(1, -1);\n out.set(name, value);\n }\n return out;\n}\n\nexport interface CookieAttributes {\n maxAgeSec: number;\n secure: boolean;\n}\n\n/** `HttpOnly; SameSite=Lax; Path=/` always; `Secure` on https. Never a `Domain`. */\nexport function serializeCookie(name: string, value: string, attrs: CookieAttributes): string {\n const parts = [`${name}=${value}`, \"Path=/\", \"HttpOnly\", \"SameSite=Lax\", `Max-Age=${Math.max(0, Math.floor(attrs.maxAgeSec))}`];\n if (attrs.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n}\n"],"mappings":";AAKA,IAAM,UAAU,IAAI,YAAY;AAChC,IAAM,UAAU,IAAI,YAAY;AAKzB,SAAS,KAAK,MAAqB;AACxC,SAAO,QAAQ,OAAO,IAAI;AAC5B;AAEO,SAAS,SAAS,OAA2B;AAClD,SAAO,QAAQ,OAAO,KAAK;AAC7B;AAEO,SAAS,UAAU,OAA2B;AACnD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,IAAK,WAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAC7E,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAGO,SAAS,cAAc,MAA4B;AACxD,MAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,QAAO;AAC3C,QAAM,SAAS,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,IAAI,MAAM,OAAO,KAAK,SAAS,KAAK,CAAC;AAC7F,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,MAAM,IAAI,WAAW,OAAO,MAAM;AACxC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,KAAI,CAAC,IAAI,OAAO,WAAW,CAAC;AACpE,SAAO;AACT;AAEO,SAAS,YAAY,QAAuB;AACjD,QAAM,MAAM,IAAI,WAAW,MAAM;AACjC,SAAO,gBAAgB,GAAG;AAC1B,SAAO;AACT;AAGO,SAAS,cAAsB;AACpC,SAAO,UAAU,YAAY,EAAE,CAAC;AAClC;AAGA,eAAsB,cAAc,UAAmC;AACrE,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,QAAQ,CAAC;AACnE,SAAO,UAAU,IAAI,WAAW,MAAM,CAAC;AACzC;AAGO,SAAS,kBAAkB,GAAW,GAAoB;AAC/D,QAAM,IAAI,KAAK,CAAC;AAChB,QAAM,IAAI,KAAK,CAAC;AAChB,MAAI,OAAO,EAAE,SAAS,EAAE;AACxB,WAAS,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAK,UAAS,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,KAAK;AACtF,SAAO,SAAS;AAClB;AAGO,SAAS,aAAa,QAA4C;AACvE,QAAM,MAAM,oBAAI,IAAoB;AACpC,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,KAAK,EAAG;AACZ,UAAM,OAAO,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACpC,QAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,EAAG;AAC5B,QAAI,QAAQ,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AACpC,QAAI,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,EAAG,SAAQ,MAAM,MAAM,GAAG,EAAE;AAC3E,QAAI,IAAI,MAAM,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAQO,SAAS,gBAAgB,MAAc,OAAe,OAAiC;AAC5F,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,UAAU,YAAY,gBAAgB,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE;AAC9H,MAAI,MAAM,OAAQ,OAAM,KAAK,QAAQ;AACrC,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/connect/browser.ts
17
+ var browser_exports = {};
18
+ module.exports = __toCommonJS(browser_exports);
19
+ throw new Error(
20
+ "@synap-core/sdk/connect is server-only: it holds the pod's tokens and the app secret. Import it from route handlers, proxy.ts or server code, never from a client component."
21
+ );
22
+ //# sourceMappingURL=connect.browser.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/browser.ts"],"sourcesContent":["/**\n * What a BROWSER bundle gets for `@synap-core/sdk/connect` (the `browser`\n * export condition): no exports, and a throw on load. See ./guard.ts.\n */\nthrow new Error(\n \"@synap-core/sdk/connect is server-only: it holds the pod's tokens and the app secret. \" +\n \"Import it from route handlers, proxy.ts or server code, never from a client component.\",\n);\n\nexport {};\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;AAIA,MAAM,IAAI;AAAA,EACR;AAEF;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,5 @@
1
+ // src/connect/browser.ts
2
+ throw new Error(
3
+ "@synap-core/sdk/connect is server-only: it holds the pod's tokens and the app secret. Import it from route handlers, proxy.ts or server code, never from a client component."
4
+ );
5
+ //# sourceMappingURL=connect.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connect/browser.ts"],"sourcesContent":["/**\n * What a BROWSER bundle gets for `@synap-core/sdk/connect` (the `browser`\n * export condition): no exports, and a throw on load. See ./guard.ts.\n */\nthrow new Error(\n \"@synap-core/sdk/connect is server-only: it holds the pod's tokens and the app secret. \" +\n \"Import it from route handlers, proxy.ts or server code, never from a client component.\",\n);\n\nexport {};\n"],"mappings":";AAIA,MAAM,IAAI;AAAA,EACR;AAEF;","names":[]}