@synap-core/sdk 1.4.0 → 1.5.1

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/README.md +152 -10
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-C5sF20f1.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-C5sF20f1.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +419 -64
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +416 -32
  10. package/dist/connect.d.ts +416 -32
  11. package/dist/connect.js +414 -65
  12. package/dist/connect.js.map +1 -1
  13. package/dist/public.cjs +9 -2
  14. package/dist/public.cjs.map +1 -1
  15. package/dist/public.d.cts +19 -4
  16. package/dist/public.d.ts +19 -4
  17. package/dist/public.js +8 -2
  18. package/dist/public.js.map +1 -1
  19. package/dist/testing.cjs +350 -15
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +171 -27
  22. package/dist/testing.d.ts +171 -27
  23. package/dist/testing.js +346 -15
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-C2KGNXEn.d.cts → types-DZYNBCq7.d.cts} +28 -5
  26. package/dist/{types-C2KGNXEn.d.ts → types-DZYNBCq7.d.ts} +28 -5
  27. package/package.json +30 -6
  28. package/src/connect/auth.ts +169 -27
  29. package/src/connect/client.ts +377 -34
  30. package/src/connect/cloud.ts +14 -10
  31. package/src/connect/discovery.ts +66 -32
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +7 -0
  34. package/src/connect/flow.ts +42 -14
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +271 -0
  37. package/src/connect/index.ts +61 -42
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/store.ts +9 -5
  40. package/src/connect/types.ts +28 -4
  41. package/src/public/client.ts +14 -2
  42. package/src/public/safe-markdown.ts +13 -0
  43. package/src/public/share-model.ts +6 -2
  44. package/src/public/site-model.ts +5 -3
  45. package/src/testing/auth-server.ts +350 -17
  46. package/src/testing/cloud.ts +3 -4
  47. package/src/testing/fixtures.ts +26 -0
  48. package/src/testing/index.ts +8 -6
  49. package/src/testing/roles.ts +27 -0
  50. package/src/testing/seed.ts +196 -0
  51. package/dist/chunk-J7XHAAFJ.js.map +0 -1
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The mock pod's data as ONE JSON-serialisable shape, so an app's mock mode,
3
+ * smoke and screenshots replay a recorded snapshot (one file) instead of
4
+ * fixtures built in app code:
5
+ *
6
+ * import { readFileSync } from "node:fs";
7
+ * const seed = parseMockPodSeed(readFileSync("mock/pod-seed.json", "utf8"));
8
+ * const pod = createMockAuthServer({ seed });
9
+ *
10
+ * Rows are the pod's wire shapes (see `@synap-core/sdk/connect` hub-types):
11
+ * `entities` as `GET /entities` answers them, `sessions` as `GET
12
+ * /focus-sessions` rows (missing columns get the pod's defaults), `documents`
13
+ * as `GET /documents/:id` (`sections` and `diagnostics` answered empty),
14
+ * `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
15
+ * as `GET /proposals?view=basic` rows, `spaces` name each granted space's
16
+ * template (`synap_grant[].templateSlug` on the token response).
17
+ *
18
+ * An entity's roles ride on its row as `facets`, the shape `GET /entities?
19
+ * includeFacets=true` answers them (`{ facetId, slug, properties, status }`),
20
+ * plus the seed-only space each is stamped in, so a seed row reads like a pod
21
+ * row read with includeFacets. The mock derives `facetSlugs` and the reach rule
22
+ * from them and serves neither field unless asked, as the pod.
23
+ */
24
+ import type { MockProposal } from "./auth-server";
25
+ import { desugarSeedRoles } from "./roles";
26
+
27
+ /** One role an entity wears (backend `FacetRowAnnotation`, plus the space it is stamped in). */
28
+ export interface MockSeedFacet {
29
+ facetId: string;
30
+ /** The role's slug, e.g. `lead`. */
31
+ slug: string;
32
+ /** The facet's overlay properties. Default `{}`. */
33
+ properties?: Record<string, unknown>;
34
+ /** Its lifecycle status. Default null. */
35
+ status?: string | null;
36
+ /**
37
+ * Seed only, never served: the space the role is in, i.e. the space whose
38
+ * members see the facet (stamped there, or a pod-wide facet of a role granted
39
+ * to it). A role in a granted space puts a pod-wide entity in the app's
40
+ * reach; a workspace lens sees the roles of that space only.
41
+ */
42
+ workspaceId: string;
43
+ /**
44
+ * Seed only, never served: the role is SHARED, i.e. stored pod-wide (null
45
+ * `workspaceId`) and granted to `workspaceId`, which then means "the space it
46
+ * is granted to". Default false = stamped in `workspaceId`. The pod treats the
47
+ * two alike for reach, except `includePodWide=false` on a space lens, which
48
+ * drops a pod-wide row reached only through a shared role.
49
+ */
50
+ sharedRole?: boolean;
51
+ }
52
+
53
+ export interface MockSeedEntity {
54
+ id: string;
55
+ profileSlug?: string;
56
+ workspaceId?: string | null;
57
+ title?: string | null;
58
+ properties?: Record<string, unknown>;
59
+ /**
60
+ * The roles (facets) it wears, in the wire shape plus the space each is in
61
+ * (`MockSeedFacet`). SEED INPUT ONLY: the pod sends them only to
62
+ * `includeFacets`, and so does the mock. A pod-wide entity (`workspaceId:
63
+ * null`) with a role in a granted space is readable by the app, as a member
64
+ * of that space sees it. Absent = none.
65
+ */
66
+ facets?: MockSeedFacet[];
67
+ /**
68
+ * @deprecated Use `facets`; removed in 2.0. Shorthand for `facets` entries
69
+ * with no id and no overlay: `parseMockPodSeed` turns each into a facet (a
70
+ * generated `facetId`, empty properties, a null status). A role stated in
71
+ * both is refused.
72
+ */
73
+ seedRoles?: Array<{ profileSlug: string; workspaceId: string }>;
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ export interface MockSeedSession {
78
+ id: string;
79
+ workspaceId?: string | null;
80
+ projectId?: string | null;
81
+ goal?: string;
82
+ status?: string;
83
+ /** Slots; `owner: "human"` ones are what waits on the person. */
84
+ expectedOutputs?: Array<Record<string, unknown>>;
85
+ /** The entity it is about: listed among that entity's connections. */
86
+ subjectEntityId?: string | null;
87
+ /** ISO 8601; the list is newest first by it. */
88
+ startedAt?: string;
89
+ [key: string]: unknown;
90
+ }
91
+
92
+ export interface MockSeedDocument {
93
+ id: string;
94
+ title: string;
95
+ content: string;
96
+ type?: string;
97
+ language?: string | null;
98
+ workspaceId?: string | null;
99
+ revision?: number;
100
+ version?: number;
101
+ createdAt?: string;
102
+ updatedAt?: string;
103
+ }
104
+
105
+ export interface MockSeedRelation {
106
+ id: string;
107
+ sourceEntityId: string;
108
+ targetEntityId: string;
109
+ type: string;
110
+ createdAt?: string;
111
+ }
112
+
113
+ export interface MockSeedSpace {
114
+ id: string;
115
+ name: string;
116
+ /** The template it came from (e.g. `content-os`); absent = made by hand. */
117
+ templateSlug?: string;
118
+ }
119
+
120
+ export interface MockPodSeed {
121
+ entities?: MockSeedEntity[];
122
+ sessions?: MockSeedSession[];
123
+ documents?: MockSeedDocument[];
124
+ relations?: MockSeedRelation[];
125
+ /** Missing fields default to a pending `create` of an `entity`, with null ids and the seed's load time. */
126
+ proposals?: Array<Partial<MockProposal> & { id: string }>;
127
+ spaces?: MockSeedSpace[];
128
+ }
129
+
130
+ const KEYS = ["entities", "sessions", "documents", "relations", "proposals", "spaces"] as const;
131
+
132
+ /** Required string fields per collection (the rest is free-form, as the pod's rows). */
133
+ const REQUIRED: Record<(typeof KEYS)[number], readonly string[]> = {
134
+ entities: ["id"],
135
+ sessions: ["id"],
136
+ documents: ["id", "title", "content"],
137
+ relations: ["id", "sourceEntityId", "targetEntityId", "type"],
138
+ proposals: ["id"],
139
+ spaces: ["id", "name"],
140
+ };
141
+
142
+ /**
143
+ * Read a seed (JSON text or an already-parsed value) and check its shape:
144
+ * every collection optional, each an array of objects carrying its required
145
+ * string fields; unknown top-level keys are refused (a typo would otherwise
146
+ * seed nothing). Throws `TypeError` naming the first bad path.
147
+ */
148
+ export function parseMockPodSeed(input: string | unknown): MockPodSeed {
149
+ const value: unknown = typeof input === "string" ? JSON.parse(input) : input;
150
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("seed: expected an object");
151
+ const obj = value as Record<string, unknown>;
152
+ for (const key of Object.keys(obj)) {
153
+ if (!(KEYS as readonly string[]).includes(key)) throw new TypeError(`seed.${key}: unknown collection (expected ${KEYS.join(", ")})`);
154
+ }
155
+ for (const key of KEYS) {
156
+ const rows = obj[key];
157
+ if (rows === undefined) continue;
158
+ if (!Array.isArray(rows)) throw new TypeError(`seed.${key}: expected an array`);
159
+ rows.forEach((row, i) => {
160
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError(`seed.${key}[${i}]: expected an object`);
161
+ for (const field of REQUIRED[key]) {
162
+ if (typeof (row as Record<string, unknown>)[field] !== "string") throw new TypeError(`seed.${key}[${i}].${field}: expected a string`);
163
+ }
164
+ if (key === "entities") checkFacets((row as Record<string, unknown>).facets, `seed.entities[${i}].facets`);
165
+ });
166
+ }
167
+ (obj.entities as Array<Record<string, unknown>> | undefined)?.forEach((row, i) => {
168
+ const roles = row.seedRoles;
169
+ if (roles === undefined) return;
170
+ if (!Array.isArray(roles)) throw new TypeError(`seed.entities[${i}].seedRoles: expected an array`);
171
+ roles.forEach((r, j) => {
172
+ for (const field of ["profileSlug", "workspaceId"]) {
173
+ if (!r || typeof r !== "object" || typeof (r as Record<string, unknown>)[field] !== "string") {
174
+ throw new TypeError(`seed.entities[${i}].seedRoles[${j}].${field}: expected a string`);
175
+ }
176
+ }
177
+ });
178
+ });
179
+ // A JSON round trip: what the mock serves is what a file could hold. Then ONE role model: seedRoles → facets.
180
+ const out = JSON.parse(JSON.stringify(obj)) as MockPodSeed;
181
+ if (out.entities) out.entities = out.entities.map((row, i) => desugarSeedRoles(row, `seed.entities[${i}]`) as MockSeedEntity);
182
+ return out;
183
+ }
184
+
185
+ function checkFacets(facets: unknown, at: string): void {
186
+ if (facets === undefined) return;
187
+ if (!Array.isArray(facets)) throw new TypeError(`${at}: expected an array`);
188
+ facets.forEach((f, j) => {
189
+ if (!f || typeof f !== "object" || Array.isArray(f)) throw new TypeError(`${at}[${j}]: expected an object`);
190
+ for (const field of ["facetId", "slug", "workspaceId"]) {
191
+ if (typeof (f as Record<string, unknown>)[field] !== "string") throw new TypeError(`${at}[${j}].${field}: expected a string`);
192
+ }
193
+ const shared = (f as Record<string, unknown>).sharedRole;
194
+ if (shared !== undefined && typeof shared !== "boolean") throw new TypeError(`${at}[${j}].sharedRole: expected a boolean`);
195
+ });
196
+ }
@@ -1 +0,0 @@
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":[]}