@secondlayer/sdk 3.5.2 → 3.5.4

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
@@ -172,6 +172,17 @@ for await (const transfer of sl.index.ftTransfers.walk({
172
172
 
173
173
  Deploy and query app-specific L3 tables.
174
174
 
175
+ Subgraphs and subscriptions live on per-tenant containers (`https://<slug>.api.secondlayer.tools`), not on the platform `api.secondlayer.tools`. The tenant containers expect a short-lived HS256 JWT, not your platform `sk-sl_*` key — so the SDK transparently mints one on the first subgraph or subscription call (POST `/api/tenants/me/keys/mint-ephemeral`), caches the apiUrl + JWT for the session's lifetime, and refreshes 30 s before expiry. You don't need to know the URL or manage tokens — just pass your normal `apiKey`.
176
+
177
+ If you already know your tenant URL (OSS, staging, or a custom routing setup), skip the lookup with `tenantBaseUrl`:
178
+
179
+ ```typescript
180
+ const sl = new SecondLayer({
181
+ apiKey: "sk-sl_...",
182
+ tenantBaseUrl: "https://myslug.api.secondlayer.tools", // optional
183
+ });
184
+ ```
185
+
175
186
  ```typescript
176
187
  // List
177
188
  const { data } = await sl.subgraphs.list();
@@ -243,7 +254,14 @@ try {
243
254
  } catch (err) {
244
255
  if (err instanceof ApiError) {
245
256
  console.log(err.status); // 404
246
- console.log(err.message); // "Contract not found"
257
+ console.log(err.code); // "NOT_FOUND" (from API's {error, code} envelope, if present)
258
+ console.log(err.message); // "Subgraph not found"
259
+ console.log(err.body); // full parsed envelope
247
260
  }
248
261
  }
249
262
  ```
263
+
264
+ Tenant-resolution failures surface as `ApiError` with distinctive codes:
265
+
266
+ - `code: "TENANT_SUSPENDED"` — your tenant is suspended (see `err.message` for the limit reason)
267
+ - `code: "NO_TENANT"` — your account has no provisioned tenant yet
package/dist/index.d.ts CHANGED
@@ -4,23 +4,61 @@ import { SubgraphAgentSchema, SubgraphSpecOptions } from "@secondlayer/shared/su
4
4
  import { InferSubgraphClient } from "@secondlayer/subgraphs";
5
5
  type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
6
  interface SecondLayerOptions {
7
- /** Base URL of the Secondlayer API (trailing slashes are stripped). */
7
+ /** Base URL of the Secondlayer platform API (trailing slashes are stripped). */
8
8
  baseUrl: string;
9
9
  /** Bearer token for authenticated requests. */
10
10
  apiKey?: string;
11
+ /**
12
+ * Explicit tenant API base URL — bypass the auto-resolution that calls
13
+ * `/api/tenants/me` on first tenant-resource request. Use when you already
14
+ * know your tenant URL (OSS, staging, or any custom routing setup).
15
+ */
16
+ tenantBaseUrl?: string;
11
17
  /** Fetch implementation. Tests and edge runtimes can provide their own. */
12
18
  fetchImpl?: FetchLike;
13
19
  /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */
14
20
  origin?: "cli" | "mcp" | "session";
15
21
  }
22
+ type TenantSession = {
23
+ apiUrl: string
24
+ token: string
25
+ expiresAtMs: number
26
+ };
16
27
  declare abstract class BaseClient {
17
28
  protected baseUrl: string;
18
29
  protected apiKey?: string;
19
30
  protected origin: "cli" | "mcp" | "session";
31
+ protected tenantBaseUrlOverride?: string;
32
+ private _tenantSession;
33
+ private _tenantSessionPromise;
20
34
  constructor(options?: Partial<SecondLayerOptions>);
21
35
  static authHeaders(apiKey?: string): Record<string, string>;
22
36
  protected request<T>(method: string, path: string, body?: unknown): Promise<T>;
37
+ protected requestAt<T>(baseUrl: string, method: string, path: string, body?: unknown, authToken?: string): Promise<T>;
23
38
  protected requestText(method: string, path: string, body?: unknown): Promise<string>;
39
+ /**
40
+ * Resolve + cache a tenant session for tenant-resource calls (subgraphs,
41
+ * subscriptions). On the platform API, those routes are not mounted —
42
+ * they live on per-tenant containers at `https://<slug>.api.secondlayer.tools`,
43
+ * which expect a short-lived HS256 JWT (not the platform `sk-sl_*` key).
44
+ *
45
+ * `POST /api/tenants/me/keys/mint-ephemeral` returns both the tenant `apiUrl`
46
+ * and a 5-min `serviceKey` JWT in one round-trip. We cache the session and
47
+ * refresh before expiry. Failures are NOT cached, so a flaky platform call
48
+ * doesn't permanently break the SDK.
49
+ *
50
+ * Bypass via `tenantBaseUrl` constructor option for OSS / staging / custom
51
+ * routing where the same `apiKey` works against both surfaces.
52
+ */
53
+ protected getTenantSession(): Promise<TenantSession>;
54
+ /**
55
+ * Returns just the tenant API base URL. Convenience wrapper around
56
+ * `getTenantSession` for callers that don't need the auth token (e.g. tests).
57
+ */
58
+ protected getTenantBaseUrl(): Promise<string>;
59
+ private mintTenantSession;
60
+ protected requestAtTenant<T>(method: string, path: string, body?: unknown): Promise<T>;
61
+ protected requestTextAtTenant(method: string, path: string, body?: unknown): Promise<string>;
24
62
  private fetchResponse;
25
63
  }
26
64
  interface SubgraphSource {
@@ -475,7 +513,9 @@ declare class ApiError extends Error {
475
513
  status: number;
476
514
  /** Raw response body (parsed JSON if possible) — preserved for callers that need error details. */
477
515
  body?: unknown;
478
- constructor(status: number, message: string, body?: unknown);
516
+ /** Stable machine-readable code from the API's `{error, code}` error envelope. */
517
+ code?: string;
518
+ constructor(status: number, message: string, body?: unknown, code?: string);
479
519
  }
480
520
  /**
481
521
  * Thrown on optimistic-concurrency conflict when a deploy supplies an
package/dist/index.js CHANGED
@@ -2,10 +2,12 @@
2
2
  class ApiError extends Error {
3
3
  status;
4
4
  body;
5
- constructor(status, message, body) {
5
+ code;
6
+ constructor(status, message, body, code) {
6
7
  super(message);
7
8
  this.status = status;
8
9
  this.body = body;
10
+ this.code = code;
9
11
  this.name = "ApiError";
10
12
  }
11
13
  }
@@ -23,15 +25,20 @@ class VersionConflictError extends ApiError {
23
25
 
24
26
  // src/base.ts
25
27
  var DEFAULT_BASE_URL = "https://api.secondlayer.tools";
28
+ var TENANT_JWT_REFRESH_BUFFER_MS = 30000;
26
29
 
27
30
  class BaseClient {
28
31
  baseUrl;
29
32
  apiKey;
30
33
  origin;
34
+ tenantBaseUrlOverride;
35
+ _tenantSession = null;
36
+ _tenantSessionPromise = null;
31
37
  constructor(options = {}) {
32
38
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
33
39
  this.apiKey = options.apiKey;
34
40
  this.origin = options.origin ?? "cli";
41
+ this.tenantBaseUrlOverride = options.tenantBaseUrl?.replace(/\/+$/, "");
35
42
  }
36
43
  static authHeaders(apiKey) {
37
44
  const headers = {
@@ -43,19 +50,71 @@ class BaseClient {
43
50
  return headers;
44
51
  }
45
52
  async request(method, path, body) {
46
- const response = await this.fetchResponse(method, path, body);
53
+ return this.requestAt(this.baseUrl, method, path, body);
54
+ }
55
+ async requestAt(baseUrl, method, path, body, authToken) {
56
+ const response = await this.fetchResponse(baseUrl, method, path, body, authToken);
47
57
  if (response.status === 204) {
48
58
  return;
49
59
  }
50
60
  return response.json();
51
61
  }
52
62
  async requestText(method, path, body) {
53
- const response = await this.fetchResponse(method, path, body);
63
+ const response = await this.fetchResponse(this.baseUrl, method, path, body);
54
64
  return response.text();
55
65
  }
56
- async fetchResponse(method, path, body) {
57
- const url = `${this.baseUrl}${path}`;
58
- const headers = BaseClient.authHeaders(this.apiKey);
66
+ async getTenantSession() {
67
+ if (this.tenantBaseUrlOverride) {
68
+ return {
69
+ apiUrl: this.tenantBaseUrlOverride,
70
+ token: this.apiKey ?? "",
71
+ expiresAtMs: Number.POSITIVE_INFINITY
72
+ };
73
+ }
74
+ const cached = this._tenantSession;
75
+ if (cached && cached.expiresAtMs - Date.now() > TENANT_JWT_REFRESH_BUFFER_MS) {
76
+ return cached;
77
+ }
78
+ if (!this._tenantSessionPromise) {
79
+ this._tenantSessionPromise = this.mintTenantSession().catch((err) => {
80
+ this._tenantSessionPromise = null;
81
+ throw err;
82
+ });
83
+ }
84
+ return this._tenantSessionPromise;
85
+ }
86
+ async getTenantBaseUrl() {
87
+ return (await this.getTenantSession()).apiUrl;
88
+ }
89
+ async mintTenantSession() {
90
+ const body = await this.request("POST", "/api/tenants/me/keys/mint-ephemeral");
91
+ if (!body.apiUrl) {
92
+ throw new ApiError(404, "No tenant API URL available for this account. Provision a tenant at https://secondlayer.tools/platform.", body, "NO_TENANT");
93
+ }
94
+ if (!body.serviceKey) {
95
+ throw new ApiError(500, "Tenant mint-ephemeral returned no serviceKey.", body, "NO_TENANT_TOKEN");
96
+ }
97
+ const session = {
98
+ apiUrl: body.apiUrl.replace(/\/+$/, ""),
99
+ token: body.serviceKey,
100
+ expiresAtMs: Date.parse(body.expiresAt)
101
+ };
102
+ this._tenantSession = session;
103
+ this._tenantSessionPromise = null;
104
+ return session;
105
+ }
106
+ async requestAtTenant(method, path, body) {
107
+ const session = await this.getTenantSession();
108
+ return this.requestAt(session.apiUrl, method, path, body, session.token);
109
+ }
110
+ async requestTextAtTenant(method, path, body) {
111
+ const session = await this.getTenantSession();
112
+ const response = await this.fetchResponse(session.apiUrl, method, path, body, session.token);
113
+ return response.text();
114
+ }
115
+ async fetchResponse(baseUrl, method, path, body, authToken) {
116
+ const url = `${baseUrl}${path}`;
117
+ const headers = BaseClient.authHeaders(authToken ?? this.apiKey);
59
118
  headers["x-sl-origin"] = this.origin;
60
119
  let response;
61
120
  try {
@@ -65,7 +124,7 @@ class BaseClient {
65
124
  body: body ? JSON.stringify(body) : undefined
66
125
  });
67
126
  } catch {
68
- throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`);
127
+ throw new ApiError(0, `Cannot reach API at ${baseUrl}. Check your connection or try again.`);
69
128
  }
70
129
  if (!response.ok) {
71
130
  if (response.status === 401) {
@@ -77,11 +136,12 @@ class BaseClient {
77
136
  throw new ApiError(429, msg);
78
137
  }
79
138
  if (response.status >= 500) {
80
- throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
139
+ throw new ApiError(response.status, `Server error. Try again or check status at ${baseUrl}/health`);
81
140
  }
82
141
  const errorBody = await response.text();
83
142
  let message = `HTTP ${response.status}`;
84
143
  let parsedBody = errorBody;
144
+ let code;
85
145
  try {
86
146
  const json = JSON.parse(errorBody);
87
147
  parsedBody = json;
@@ -91,11 +151,14 @@ class BaseClient {
91
151
  } else if (err && typeof err === "object") {
92
152
  message = JSON.stringify(err);
93
153
  }
154
+ if (typeof json.code === "string") {
155
+ code = json.code;
156
+ }
94
157
  } catch {
95
158
  if (errorBody)
96
159
  message = errorBody;
97
160
  }
98
- throw new ApiError(response.status, message, parsedBody);
161
+ throw new ApiError(response.status, message, parsedBody, code);
99
162
  }
100
163
  return response;
101
164
  }
@@ -173,28 +236,28 @@ function buildSpecQueryString(options) {
173
236
 
174
237
  class Subgraphs extends BaseClient {
175
238
  async list() {
176
- return this.request("GET", "/api/subgraphs");
239
+ return this.requestAtTenant("GET", "/api/subgraphs");
177
240
  }
178
241
  async get(name) {
179
- return this.request("GET", `/api/subgraphs/${name}`);
242
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}`);
180
243
  }
181
244
  async openapi(name, options) {
182
- return this.request("GET", `/api/subgraphs/${name}/openapi.json${buildSpecQueryString(options)}`);
245
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}/openapi.json${buildSpecQueryString(options)}`);
183
246
  }
184
247
  async schema(name, options) {
185
- return this.request("GET", `/api/subgraphs/${name}/schema.json${buildSpecQueryString(options)}`);
248
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}/schema.json${buildSpecQueryString(options)}`);
186
249
  }
187
250
  async markdown(name, options) {
188
- return this.requestText("GET", `/api/subgraphs/${name}/docs.md${buildSpecQueryString(options)}`);
251
+ return this.requestTextAtTenant("GET", `/api/subgraphs/${name}/docs.md${buildSpecQueryString(options)}`);
189
252
  }
190
253
  async reindex(name, options) {
191
- return this.request("POST", `/api/subgraphs/${name}/reindex`, options);
254
+ return this.requestAtTenant("POST", `/api/subgraphs/${name}/reindex`, options);
192
255
  }
193
256
  async stop(name) {
194
- return this.request("POST", `/api/subgraphs/${name}/stop`);
257
+ return this.requestAtTenant("POST", `/api/subgraphs/${name}/stop`);
195
258
  }
196
259
  async backfill(name, options) {
197
- return this.request("POST", `/api/subgraphs/${name}/backfill`, options);
260
+ return this.requestAtTenant("POST", `/api/subgraphs/${name}/backfill`, options);
198
261
  }
199
262
  async gaps(name, opts) {
200
263
  const qs = new URLSearchParams;
@@ -205,27 +268,27 @@ class Subgraphs extends BaseClient {
205
268
  if (opts?.resolved !== undefined)
206
269
  qs.set("resolved", String(opts.resolved));
207
270
  const query = qs.toString();
208
- return this.request("GET", `/api/subgraphs/${name}/gaps${query ? `?${query}` : ""}`);
271
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}/gaps${query ? `?${query}` : ""}`);
209
272
  }
210
273
  async delete(name, options) {
211
274
  const qs = options?.force ? "?force=true" : "";
212
- return this.request("DELETE", `/api/subgraphs/${name}${qs}`);
275
+ return this.requestAtTenant("DELETE", `/api/subgraphs/${name}${qs}`);
213
276
  }
214
277
  async deploy(data) {
215
- return this.request("POST", "/api/subgraphs", data);
278
+ return this.requestAtTenant("POST", "/api/subgraphs", data);
216
279
  }
217
280
  async getSource(name) {
218
- return this.request("GET", `/api/subgraphs/${name}/source`);
281
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}/source`);
219
282
  }
220
283
  async bundle(data) {
221
- return this.request("POST", "/api/subgraphs/bundle", data);
284
+ return this.requestAtTenant("POST", "/api/subgraphs/bundle", data);
222
285
  }
223
286
  async queryTable(name, table, params = {}) {
224
- const result = await this.request("GET", `/api/subgraphs/${name}/${table}${buildSubgraphQueryString(params)}`);
287
+ const result = await this.requestAtTenant("GET", `/api/subgraphs/${name}/${table}${buildSubgraphQueryString(params)}`);
225
288
  return Array.isArray(result) ? result : result.data;
226
289
  }
227
290
  async queryTableCount(name, table, params = {}) {
228
- return this.request("GET", `/api/subgraphs/${name}/${table}/count${buildSubgraphQueryString(params)}`);
291
+ return this.requestAtTenant("GET", `/api/subgraphs/${name}/${table}/count${buildSubgraphQueryString(params)}`);
229
292
  }
230
293
  typed(def) {
231
294
  const result = {};
@@ -648,40 +711,40 @@ function createStreamsClient(options) {
648
711
  // src/subscriptions/client.ts
649
712
  class Subscriptions extends BaseClient {
650
713
  async list() {
651
- return this.request("GET", "/api/subscriptions");
714
+ return this.requestAtTenant("GET", "/api/subscriptions");
652
715
  }
653
716
  async get(id) {
654
- return this.request("GET", `/api/subscriptions/${id}`);
717
+ return this.requestAtTenant("GET", `/api/subscriptions/${id}`);
655
718
  }
656
719
  async create(input) {
657
- return this.request("POST", "/api/subscriptions", input);
720
+ return this.requestAtTenant("POST", "/api/subscriptions", input);
658
721
  }
659
722
  async update(id, patch) {
660
- return this.request("PATCH", `/api/subscriptions/${id}`, patch);
723
+ return this.requestAtTenant("PATCH", `/api/subscriptions/${id}`, patch);
661
724
  }
662
725
  async pause(id) {
663
- return this.request("POST", `/api/subscriptions/${id}/pause`);
726
+ return this.requestAtTenant("POST", `/api/subscriptions/${id}/pause`);
664
727
  }
665
728
  async resume(id) {
666
- return this.request("POST", `/api/subscriptions/${id}/resume`);
729
+ return this.requestAtTenant("POST", `/api/subscriptions/${id}/resume`);
667
730
  }
668
731
  async delete(id) {
669
- return this.request("DELETE", `/api/subscriptions/${id}`);
732
+ return this.requestAtTenant("DELETE", `/api/subscriptions/${id}`);
670
733
  }
671
734
  async rotateSecret(id) {
672
- return this.request("POST", `/api/subscriptions/${id}/rotate-secret`);
735
+ return this.requestAtTenant("POST", `/api/subscriptions/${id}/rotate-secret`);
673
736
  }
674
737
  async recentDeliveries(id) {
675
- return this.request("GET", `/api/subscriptions/${id}/deliveries`);
738
+ return this.requestAtTenant("GET", `/api/subscriptions/${id}/deliveries`);
676
739
  }
677
740
  async replay(id, range) {
678
- return this.request("POST", `/api/subscriptions/${id}/replay`, range);
741
+ return this.requestAtTenant("POST", `/api/subscriptions/${id}/replay`, range);
679
742
  }
680
743
  async dead(id) {
681
- return this.request("GET", `/api/subscriptions/${id}/dead`);
744
+ return this.requestAtTenant("GET", `/api/subscriptions/${id}/dead`);
682
745
  }
683
746
  async requeueDead(id, outboxId) {
684
- return this.request("POST", `/api/subscriptions/${id}/dead/${outboxId}/requeue`);
747
+ return this.requestAtTenant("POST", `/api/subscriptions/${id}/dead/${outboxId}/requeue`);
685
748
  }
686
749
  }
687
750
 
@@ -872,5 +935,5 @@ export {
872
935
  ApiError
873
936
  };
874
937
 
875
- //# debugId=89BE7A17BC42C82764756E2164756E21
938
+ //# debugId=324E81D5B88A8D7F64756E2164756E21
876
939
  //# sourceMappingURL=index.js.map