@maximem/synap-js-sdk 0.4.1 → 0.4.3

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/dist/index.d.cts CHANGED
@@ -299,6 +299,18 @@ declare class HttpTransport {
299
299
  currentCredentials(): Credentials;
300
300
  /** Write back identity resolved from `GET /api/v1/auth/whoami`. */
301
301
  updateCredentials(patch: Partial<Credentials>): void;
302
+ /**
303
+ * The instance's scoping mode, from whoami. Lives here rather than on the
304
+ * client because every interface factory already receives the transport,
305
+ * and it is per-client, so a module-level global would be wrong the moment a
306
+ * process holds two clients.
307
+ *
308
+ * undefined means the server did not report it, which is true of any
309
+ * deployment older than the field, and must mean "enforce nothing".
310
+ */
311
+ userContextIsolation: string | undefined;
312
+ /** Refuse a customer_id on a B2C instance, at the call site. */
313
+ checkCustomerId(customerId: unknown, where: string): void;
302
314
  setTimeouts(timeouts: Partial<TimeoutConfig>): void;
303
315
  /** `null` restores the default policy, matching Python's `retry_policy=None`. */
304
316
  setRetryPolicy(policy: Partial<RetryPolicy> | null): void;
package/dist/index.d.ts CHANGED
@@ -299,6 +299,18 @@ declare class HttpTransport {
299
299
  currentCredentials(): Credentials;
300
300
  /** Write back identity resolved from `GET /api/v1/auth/whoami`. */
301
301
  updateCredentials(patch: Partial<Credentials>): void;
302
+ /**
303
+ * The instance's scoping mode, from whoami. Lives here rather than on the
304
+ * client because every interface factory already receives the transport,
305
+ * and it is per-client, so a module-level global would be wrong the moment a
306
+ * process holds two clients.
307
+ *
308
+ * undefined means the server did not report it, which is true of any
309
+ * deployment older than the field, and must mean "enforce nothing".
310
+ */
311
+ userContextIsolation: string | undefined;
312
+ /** Refuse a customer_id on a B2C instance, at the call site. */
313
+ checkCustomerId(customerId: unknown, where: string): void;
302
314
  setTimeouts(timeouts: Partial<TimeoutConfig>): void;
303
315
  /** `null` restores the default policy, matching Python's `retry_policy=None`. */
304
316
  setRetryPolicy(policy: Partial<RetryPolicy> | null): void;
package/dist/index.js CHANGED
@@ -200,6 +200,16 @@ var Heartbeat = class {
200
200
  }
201
201
  };
202
202
 
203
+ // src/scoping.ts
204
+ var B2C_ISOLATION = "equals_customer";
205
+ function checkCustomerId(isolation, customerId, where, instanceId) {
206
+ if (customerId === void 0 || customerId === null || customerId === "") return;
207
+ if (isolation !== B2C_ISOLATION) return;
208
+ throw new InvalidInputError(
209
+ `${where}: customer_id is not accepted on this instance${instanceId ? ` (${instanceId})` : ""}, which is B2C (user_context_isolation='${B2C_ISOLATION}'). Send user_id only; it is the whole identity, and the server files and reads your data under it. Received customer_id=${JSON.stringify(customerId)}.`
210
+ );
211
+ }
212
+
203
213
  // src/transport/http.ts
204
214
  var DEFAULT_BASE_URL = "https://synap-cloud-prod.maximem.ai";
205
215
  var DEFAULT_TIMEOUTS = { connect: 5, read: 30, write: 10 };
@@ -253,6 +263,25 @@ var HttpTransport = class {
253
263
  updateCredentials(patch) {
254
264
  this.#credentials = { ...this.#credentials, ...patch };
255
265
  }
266
+ /**
267
+ * The instance's scoping mode, from whoami. Lives here rather than on the
268
+ * client because every interface factory already receives the transport,
269
+ * and it is per-client, so a module-level global would be wrong the moment a
270
+ * process holds two clients.
271
+ *
272
+ * undefined means the server did not report it, which is true of any
273
+ * deployment older than the field, and must mean "enforce nothing".
274
+ */
275
+ userContextIsolation = void 0;
276
+ /** Refuse a customer_id on a B2C instance, at the call site. */
277
+ checkCustomerId(customerId, where) {
278
+ checkCustomerId(
279
+ this.userContextIsolation,
280
+ customerId,
281
+ where,
282
+ this.#credentials.instanceId
283
+ );
284
+ }
256
285
  setTimeouts(timeouts) {
257
286
  this.timeouts = { ...this.timeouts, ...timeouts };
258
287
  }
@@ -527,6 +556,10 @@ function buildFetchRequest(scope, options) {
527
556
  }
528
557
  async function fetchContext(transport, scope, options = {}, hooks = {}) {
529
558
  const startedAt = Date.now();
559
+ transport.checkCustomerId(
560
+ opt(options, "customer_id", "customerId"),
561
+ `${scope}.context.fetch`
562
+ );
530
563
  const anticipated = hooks.beforeFetch?.(scope, options) ?? null;
531
564
  if (anticipated !== null) {
532
565
  hooks.onServedFromCache?.(scope, options, anticipated);
@@ -787,6 +820,8 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "error",
787
820
  function createMemoriesInterface(transport) {
788
821
  return {
789
822
  async create(options) {
823
+ const o2 = options;
824
+ transport.checkCustomerId(o2["customer_id"] ?? o2["customerId"], "memories.create");
790
825
  return transport.request("memories_create", { body: buildCreateBody(options) });
791
826
  },
792
827
  async batch_create(options) {
@@ -803,7 +838,7 @@ function createMemoriesInterface(transport) {
803
838
  const userId = options.user_id ?? options.userId;
804
839
  const customerId = options.customer_id ?? options.customerId;
805
840
  if (!userId) throw new InvalidInputError("user_id is required");
806
- if (!customerId) throw new InvalidInputError("customer_id is required");
841
+ transport.checkCustomerId(customerId, "memories.create_from_file");
807
842
  const filePath = options.file_path ?? options.filePath;
808
843
  const sources = [filePath, options.file, options.text].filter((v) => v !== void 0);
809
844
  if (sources.length === 0) {
@@ -946,7 +981,6 @@ function buildRecordMessageBody(options) {
946
981
  throw new InvalidInputError("content is required");
947
982
  }
948
983
  if (!userId) throw new InvalidInputError("user_id is required");
949
- if (!customerId) throw new InvalidInputError("customer_id is required");
950
984
  const body = {
951
985
  conversation_id: conversationId,
952
986
  role: options.role,
@@ -1051,6 +1085,10 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1051
1085
  };
1052
1086
  return {
1053
1087
  async record_message(options) {
1088
+ transport.checkCustomerId(
1089
+ options["customer_id"] ?? options["customerId"],
1090
+ "conversation.record_message"
1091
+ );
1054
1092
  const body = buildRecordMessageBody(options);
1055
1093
  const result = await transport.request("conversations_messages", { body });
1056
1094
  onTurn({
@@ -1069,6 +1107,13 @@ function createConversationNamespace(transport, subscribers = new CompactionSubs
1069
1107
  if (!Array.isArray(messages) || messages.length === 0) {
1070
1108
  throw new InvalidInputError("messages must be a non-empty array");
1071
1109
  }
1110
+ for (const m of messages) {
1111
+ const mo = m;
1112
+ transport.checkCustomerId(
1113
+ mo["customer_id"] ?? mo["customerId"],
1114
+ "conversation.record_messages_batch"
1115
+ );
1116
+ }
1072
1117
  const bodies = messages.map(buildRecordMessageBody);
1073
1118
  const result = await transport.request("conversations_messages_batch", {
1074
1119
  body: { messages: bodies }
@@ -1238,7 +1283,7 @@ function envPort() {
1238
1283
  return Number.isFinite(parsed) ? parsed : void 0;
1239
1284
  }
1240
1285
  function envTls() {
1241
- const raw = getEnv("SYNAP_GRPC_USE_TLS");
1286
+ const raw = getEnv("SYNAP_GRPC_USE_TLS") ?? getEnv("SYNAP_GRPC_TLS");
1242
1287
  if (raw === void 0 || raw === "") return void 0;
1243
1288
  return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
1244
1289
  }
@@ -2168,6 +2213,7 @@ var SynapClient = class {
2168
2213
  #fetchHooks;
2169
2214
  constructor(options = {}) {
2170
2215
  const credentials = resolveCredentials(options);
2216
+ const resolvedBaseUrl = resolveBaseUrl(options);
2171
2217
  validateInstanceId(credentials.instanceId);
2172
2218
  const registryKey = buildRegistryKey(credentials.instanceId, credentials.apiKey);
2173
2219
  if (options._force_new !== true) {
@@ -2185,7 +2231,12 @@ var SynapClient = class {
2185
2231
  this.#registryKeys = options._force_new === true ? [] : [registryKey];
2186
2232
  this.#transport = new HttpTransport({
2187
2233
  credentials,
2188
- ...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
2234
+ // SYNAP_BASE_URL is honoured, as Python does (sdk.py) and as the 0.3.x
2235
+ // wrapper did. Ignoring it was a wrong-DESTINATION bug rather than a
2236
+ // missing convenience: anyone pointing at a self-hosted or staging
2237
+ // deployment through the environment would silently have started sending
2238
+ // their data to production on upgrade.
2239
+ ...resolvedBaseUrl !== void 0 ? { baseUrl: resolvedBaseUrl } : {},
2189
2240
  ...options.timeouts !== void 0 ? { timeouts: options.timeouts } : {},
2190
2241
  ...options.retryPolicy !== void 0 ? { retryPolicy: options.retryPolicy } : {},
2191
2242
  ...options.keepAlive !== void 0 ? { keepAlive: options.keepAlive } : {},
@@ -2371,6 +2422,8 @@ var SynapClient = class {
2371
2422
  const whoami = await this.#transport.request("whoami", {});
2372
2423
  const resolvedClientId = String(whoami["client_id"] ?? "");
2373
2424
  const resolvedInstanceId = String(whoami["instance_id"] ?? "");
2425
+ const iso2 = whoami["user_context_isolation"];
2426
+ this.#transport.userContextIsolation = typeof iso2 === "string" ? iso2 : void 0;
2374
2427
  this.#transport.updateCredentials({
2375
2428
  ...resolvedClientId !== "" && credentials.clientId === "" ? { clientId: resolvedClientId } : {},
2376
2429
  ...resolvedInstanceId !== "" && credentials.instanceId === "" ? { instanceId: resolvedInstanceId } : {}
@@ -2635,6 +2688,11 @@ function pick3(o2, snake, camel) {
2635
2688
  const a = o2[snake];
2636
2689
  return a !== void 0 ? a : o2[camel];
2637
2690
  }
2691
+ function resolveBaseUrl(options) {
2692
+ if (options.baseUrl !== void 0) return options.baseUrl;
2693
+ const fromEnv = getEnv("SYNAP_BASE_URL")?.trim();
2694
+ return fromEnv !== void 0 && fromEnv !== "" ? fromEnv : void 0;
2695
+ }
2638
2696
  function resolveCredentials(options) {
2639
2697
  const apiKey = options.apiKey ?? getEnv("SYNAP_API_KEY");
2640
2698
  if (!apiKey) {