railcode 0.1.20 → 0.1.21

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
@@ -58,7 +58,7 @@ railcode agent <list|show|create|update|delete|run|schedule> ...
58
58
  `railcode db list` (aliases `ls`, `connections`) prints each connector's
59
59
  `name` + `engine` (`--json` for the raw array). `railcode db query "<sql>"`
60
60
  (alias `sql`) runs SQL against `--connection` (default `default`) and prints a
61
- table + row count; `--engine <postgres|bigquery|snowflake>` is inferred from the connector
61
+ table + row count; `--engine <postgres|bigquery|turso>` is inferred from the connector
62
62
  list when omitted, `--params '<json-array>'` binds `$1, $2, …` placeholders
63
63
  (SQL is never interpolated), `--file <path>` reads SQL from a file, and
64
64
  `--json` prints the raw `{ columns, rows, rowcount, truncated }` envelope.
package/dist/db.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // the command layer in index.ts does the HTTP, file reads, and printing.
4
4
  export class DbError extends Error {
5
5
  }
6
- export const DB_ENGINES = ["postgres", "bigquery", "snowflake"];
6
+ export const DB_ENGINES = ["postgres", "bigquery", "turso"];
7
7
  // `--params '<json-array>'` → the positional $1,$2,… values. Empty when omitted.
8
8
  export function parseSqlParams(raw) {
9
9
  if (raw === undefined)
package/dist/index.js CHANGED
@@ -826,7 +826,7 @@ export default defineConfig({
826
826
  }
827
827
  function reactRailcodeTypes() {
828
828
  return `type Me = {
829
- user: { uuid: string; name: string; email: string };
829
+ user: { uuid: string; name: string; email: string; is_admin: boolean; roles: RoleRef[] };
830
830
  app: { uuid: string; slug: string; name: string };
831
831
  org: { uuid: string; slug: string; name: string };
832
832
  };
@@ -835,9 +835,12 @@ type AppUser = {
835
835
  uuid: string;
836
836
  name: string;
837
837
  email: string;
838
- role: string | null;
838
+ is_admin: boolean;
839
839
  };
840
840
 
841
+ type RoleRef = { uuid: string; name: string };
842
+ type OrgRole = RoleRef & { description: string; is_member: boolean };
843
+
841
844
  type KvRecord<T = unknown> = {
842
845
  key: string;
843
846
  value: T;
@@ -881,7 +884,7 @@ type SqlRows = Array<Record<string, unknown>> & {
881
884
  };
882
885
 
883
886
  type DataConnectorInfo = {
884
- engine: "postgres" | "bigquery" | "snowflake" | string;
887
+ engine: "postgres" | "bigquery" | "turso" | string;
885
888
  name: string;
886
889
  };
887
890
 
@@ -960,8 +963,25 @@ type LlmModelInfo = { model: string; default: boolean };
960
963
 
961
964
  type LlmProviderInfo = { provider: string; default: boolean; models: LlmModelInfo[] };
962
965
 
966
+ type EmailSendOptions = {
967
+ to: string | string[];
968
+ subject: string;
969
+ html?: string;
970
+ text?: string;
971
+ cc?: string | string[];
972
+ bcc?: string | string[];
973
+ replyTo?: string;
974
+ };
975
+
976
+ type EmailSendResult = {
977
+ id: string;
978
+ status: string;
979
+ requestId: string;
980
+ };
981
+
963
982
  declare const me: () => Promise<Me>;
964
983
  declare const appUsers: () => Promise<AppUser[]>;
984
+ declare const roles: () => Promise<OrgRole[]>;
965
985
  declare const designSystem: () => Promise<string>;
966
986
  declare const db: { collection<T = unknown>(name: string): Collection<T> };
967
987
  declare const files: {
@@ -973,7 +993,6 @@ declare const files: {
973
993
  declare const data: DatabaseNamespace;
974
994
  declare const postgres: DatabaseNamespace;
975
995
  declare const bigquery: DatabaseNamespace;
976
- declare const snowflake: DatabaseNamespace;
977
996
  declare const dataConnectors: () => Promise<DataConnectorInfo[]>;
978
997
  declare const savedQueries: () => Promise<SavedQueryInfo[]>;
979
998
  declare const query: (name: string, params?: Record<string, unknown>) => Promise<SqlRows>;
@@ -986,6 +1005,7 @@ declare const llm: {
986
1005
  stream(input: string | LlmMessage[], opts?: LlmOptions): AsyncIterable<LlmStreamEvent>;
987
1006
  };
988
1007
  declare const llmProviders: () => Promise<LlmProviderInfo[]>;
1008
+ declare const email: { send(opts: EmailSendOptions): Promise<EmailSendResult> };
989
1009
  `;
990
1010
  }
991
1011
  function reactViteEnvTypes() {
@@ -1060,6 +1080,14 @@ function message(error: unknown): string {
1060
1080
  return error instanceof Error ? error.message : String(error);
1061
1081
  }
1062
1082
 
1083
+ function httpStatus(error: unknown): number | null {
1084
+ if (typeof error === "object" && error !== null && "status" in error) {
1085
+ const status = (error as { status?: unknown }).status;
1086
+ return typeof status === "number" ? status : null;
1087
+ }
1088
+ return null;
1089
+ }
1090
+
1063
1091
  function truncate(value: string, max: number): string {
1064
1092
  return value.length > max ? value.slice(0, max - 1) + "…" : value;
1065
1093
  }
@@ -1361,7 +1389,7 @@ function Hero() {
1361
1389
  </div>
1362
1390
  ) : null}
1363
1391
  {teammates.slice(0, 4).map((user) => (
1364
- <div className="avatar" key={user.uuid} title={user.name + (user.role ? " · " + user.role : "")}>
1392
+ <div className="avatar" key={user.uuid} title={user.name + (user.is_admin ? " · admin" : "")}>
1365
1393
  {initials(user.name)}
1366
1394
  </div>
1367
1395
  ))}
@@ -1867,9 +1895,124 @@ function ConnectorsCard() {
1867
1895
  );
1868
1896
  }
1869
1897
 
1898
+ function emailFailureMessage(error: unknown): string {
1899
+ const status = httpStatus(error);
1900
+ if (status === 403) return "Email is not granted for this app or user yet.";
1901
+ if (status === 429) return "The daily email recipient limit has been reached.";
1902
+ if (status === 503) return "Email is not configured for this Railcode instance.";
1903
+ return message(error);
1904
+ }
1905
+
1906
+ function EmailCard() {
1907
+ const { identity } = useAppStore();
1908
+ const [to, setTo] = useState("");
1909
+ const [subject, setSubject] = useState("Hello from " + APP_SLUG);
1910
+ const [body, setBody] = useState(
1911
+ "This message was sent by the Railcode starter app through the platform email gateway.",
1912
+ );
1913
+ const [busy, setBusy] = useState(false);
1914
+ const [status, setStatus] = useState<{ kind: "sent" | "error"; text: string } | null>(null);
1915
+
1916
+ useEffect(() => {
1917
+ if (!to && identity?.user.email) setTo(identity.user.email);
1918
+ }, [identity, to]);
1919
+
1920
+ async function sendMessage(event: FormEvent<HTMLFormElement>) {
1921
+ event.preventDefault();
1922
+ const recipient = to.trim();
1923
+ const trimmedSubject = subject.trim() || "Hello from " + APP_SLUG;
1924
+ const text = body.trim() || "Sent from a Railcode starter app.";
1925
+ if (!recipient) {
1926
+ setStatus({ kind: "error", text: "Add a recipient email address first." });
1927
+ return;
1928
+ }
1929
+
1930
+ setBusy(true);
1931
+ setStatus(null);
1932
+ try {
1933
+ const result = await email.send({ to: recipient, subject: trimmedSubject, text });
1934
+ setStatus({
1935
+ kind: "sent",
1936
+ text: "Sent with status " + result.status + " · " + result.requestId,
1937
+ });
1938
+ } catch (error) {
1939
+ setStatus({ kind: "error", text: emailFailureMessage(error) });
1940
+ } finally {
1941
+ setBusy(false);
1942
+ }
1943
+ }
1944
+
1945
+ return (
1946
+ <article className="card">
1947
+ <div className="card-head">
1948
+ <div>
1949
+ <div className="card-title-row">
1950
+ <Icon name="mail" />
1951
+ <span className="card-title">
1952
+ <Reveal code="await email.send(message)">Email</Reveal>
1953
+ </span>
1954
+ </div>
1955
+ </div>
1956
+ </div>
1957
+
1958
+ <form className="email-form" onSubmit={sendMessage}>
1959
+ <label className="control-field">
1960
+ <span>Recipient</span>
1961
+ <input
1962
+ className="field"
1963
+ type="email"
1964
+ autoComplete="email"
1965
+ value={to}
1966
+ onChange={(event) => setTo(event.target.value)}
1967
+ />
1968
+ </label>
1969
+ <label className="control-field">
1970
+ <span>Subject</span>
1971
+ <input
1972
+ className="field"
1973
+ type="text"
1974
+ value={subject}
1975
+ onChange={(event) => setSubject(event.target.value)}
1976
+ />
1977
+ </label>
1978
+ <label className="json-editor">
1979
+ <span>Message</span>
1980
+ <textarea
1981
+ className="field"
1982
+ rows={4}
1983
+ value={body}
1984
+ onChange={(event) => setBody(event.target.value)}
1985
+ />
1986
+ </label>
1987
+ <ActionTip code={\`await email.send({
1988
+ to,
1989
+ subject,
1990
+ text
1991
+ })\`} className="upload-tip">
1992
+ <button className="btn btn-primary" type="submit" disabled={busy}>
1993
+ {busy ? <span className="spinner" /> : <Icon name="mail" />}
1994
+ {busy ? "Sending..." : "Send test email"}
1995
+ </button>
1996
+ </ActionTip>
1997
+ </form>
1998
+
1999
+ <div className="email-state">
2000
+ <div className="result-status">
2001
+ <span>Delivery</span>
2002
+ <span>{status?.kind === "sent" ? "Sent" : status?.kind === "error" ? "Needs setup" : "Ready"}</span>
2003
+ </div>
2004
+ <p className={status?.kind === "error" ? "email-note is-error" : "email-note"}>
2005
+ {status?.text ||
2006
+ "Email uses the platform sender and requires an email grant or app manifest approval."}
2007
+ </p>
2008
+ </div>
2009
+ </article>
2010
+ );
2011
+ }
2012
+
1870
2013
  const AI_SAMPLE_PROMPT = "Summarize what this Railcode template demonstrates in one sentence.";
1871
2014
  const AI_SAMPLE_RESPONSE =
1872
- "This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, and generating text — all without handling a single credential in the browser.";
2015
+ "This template shows a live Railcode app reading identity, storing notes in KV, uploading files, querying Postgres, invoking a saved query, calling Stripe through a service connector, sending email, and generating text — all without handling a single credential in the browser.";
1873
2016
 
1874
2017
  function AiCard() {
1875
2018
  const [prompt, setPrompt] = useState(AI_SAMPLE_PROMPT);
@@ -2010,6 +2153,10 @@ function IconSprite() {
2010
2153
  <symbol id="i-chat" viewBox="0 0 24 24">
2011
2154
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
2012
2155
  </symbol>
2156
+ <symbol id="i-mail" viewBox="0 0 24 24">
2157
+ <rect x="3" y="5" width="18" height="14" rx="2" />
2158
+ <path d="m3 7 9 6 9-6" />
2159
+ </symbol>
2013
2160
  <symbol id="i-code" viewBox="0 0 24 24">
2014
2161
  <polyline points="16 18 22 12 16 6" />
2015
2162
  <polyline points="8 6 2 12 8 18" />
@@ -2076,11 +2223,18 @@ export function App() {
2076
2223
 
2077
2224
  <GuideSection
2078
2225
  title="They can connect to external services"
2079
- body="Railcode apps can securely connect to your SaaS services so you can manage refunds, send emails, view data, and anything else you need."
2226
+ body="Railcode apps can securely connect to your SaaS services so you can manage refunds, sync tickets, enrich records, and anything else you need."
2080
2227
  >
2081
2228
  <ConnectorsCard />
2082
2229
  </GuideSection>
2083
2230
 
2231
+ <GuideSection
2232
+ title="They can send email"
2233
+ body="Send transactional email through the Railcode gateway without putting provider credentials in browser code."
2234
+ >
2235
+ <EmailCard />
2236
+ </GuideSection>
2237
+
2084
2238
  <GuideSection
2085
2239
  title="They can use AI"
2086
2240
  body="Build AI-native apps that use LLMs to enable powerful workflows."
@@ -3101,6 +3255,41 @@ th:first-child {
3101
3255
  }
3102
3256
  }
3103
3257
 
3258
+ /* ---------- email card ---------- */
3259
+
3260
+ .email-form {
3261
+ display: flex;
3262
+ flex-direction: column;
3263
+ gap: var(--space-3);
3264
+ }
3265
+
3266
+ .email-form .field {
3267
+ width: 100%;
3268
+ }
3269
+
3270
+ .email-state {
3271
+ margin-top: var(--space-4);
3272
+ padding: 12px 14px;
3273
+ border: 1px solid var(--line);
3274
+ border-radius: var(--radius-md);
3275
+ background: var(--bg-inset);
3276
+ }
3277
+
3278
+ .email-state .result-status {
3279
+ margin-bottom: 6px;
3280
+ }
3281
+
3282
+ .email-note {
3283
+ color: var(--ink-3);
3284
+ font-size: 12.5px;
3285
+ line-height: 1.55;
3286
+ overflow-wrap: anywhere;
3287
+ }
3288
+
3289
+ .email-note.is-error {
3290
+ color: #b42318;
3291
+ }
3292
+
3104
3293
  /* ---------- reveal + action tooltips ---------- */
3105
3294
 
3106
3295
  .reveal {
@@ -4102,7 +4291,7 @@ List options:
4102
4291
 
4103
4292
  Query options:
4104
4293
  --connection <name> Connector to query (default: default)
4105
- --engine <postgres|mysql> Engine override (inferred from --connection otherwise)
4294
+ --engine <postgres|bigquery|turso> Engine override (inferred from --connection otherwise)
4106
4295
  --params '<json-array>' Values for $1, $2, … placeholders
4107
4296
  --file <path> Read SQL from a file instead of the argument
4108
4297
  --json Print the raw { columns, rows, rowcount, truncated } envelope
@@ -5453,6 +5642,10 @@ async function handleLocalApi(ctx, req, res, url) {
5453
5642
  sendJson(res, 200, [appUserEnvelope()]);
5454
5643
  return;
5455
5644
  }
5645
+ if (path === "/roles") {
5646
+ sendJson(res, 200, []);
5647
+ return;
5648
+ }
5456
5649
  if (path === "/config/design-system") {
5457
5650
  await handleDevDesignSystem(ctx, res);
5458
5651
  return;
@@ -5803,13 +5996,19 @@ async function handleDevDesignSystem(ctx, res) {
5803
5996
  // ── envelopes + http helpers ──────────────────────────────────────────────────
5804
5997
  function meEnvelope(ctx) {
5805
5998
  return {
5806
- user: { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost" },
5999
+ user: {
6000
+ uuid: DEV_USER_UUID,
6001
+ name: "Local Dev",
6002
+ email: "dev@localhost",
6003
+ is_admin: true,
6004
+ roles: [],
6005
+ },
5807
6006
  app: { uuid: DEV_APP_UUID, slug: ctx.app, name: titleCase(ctx.app) },
5808
6007
  org: { uuid: DEV_ORG_UUID, slug: "local", name: "Local" },
5809
6008
  };
5810
6009
  }
5811
6010
  function appUserEnvelope() {
5812
- return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost", role: "owner" };
6011
+ return { uuid: DEV_USER_UUID, name: "Local Dev", email: "dev@localhost", is_admin: true };
5813
6012
  }
5814
6013
  function isLoopbackHost(host) {
5815
6014
  if (!host)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/static/sdk.js CHANGED
@@ -35,7 +35,7 @@
35
35
  padding:2px 6px; border-radius:4px; white-space:nowrap; align-self:start; margin-top:2px; }
36
36
  .k-sql { background:#1e3a5f; color:#7cc4ff; } .k-db { background:#3a2752; color:#c79bff; }
37
37
  .k-files { background:#13413c; color:#5fd6c4; } .k-llm { background:#4a2b16; color:#f6b26b; }
38
- .k-me, .k-connections, .k-app-users { background:#2a2f38; color:#9aa4b2; }
38
+ .k-me, .k-connections, .k-app-users, .k-roles { background:#2a2f38; color:#9aa4b2; }
39
39
  .body { min-width:0; }
40
40
  code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size:11.5px; color:#e6e8eb;
41
41
  word-break:break-word; display:block; line-height:1.45; }
@@ -54,7 +54,7 @@
54
54
  <button id="collapse">Hide \u203A</button>
55
55
  </header>
56
56
  <div class="log" id="log"></div>
57
- <footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code></footer>
57
+ <footer>Calls made via <code>postgres()</code>, <code>llm</code>, <code>db</code>, <code>files</code>, <code>me()</code>, <code>appUsers()</code>, <code>roles()</code></footer>
58
58
  </div>`;
59
59
  function mount() {
60
60
  (document.body || document.documentElement).appendChild(host);
@@ -265,7 +265,7 @@
265
265
  var data = namespace();
266
266
  var postgres = namespace("postgres");
267
267
  var bigquery = namespace("bigquery");
268
- var snowflake = namespace("snowflake");
268
+ var turso = namespace("turso");
269
269
  var dataConnectors = () => track("connections", "dataConnectors()", () => call("GET", "/connections"));
270
270
 
271
271
  // ../sdk/src/util.ts
@@ -277,9 +277,18 @@
277
277
  }
278
278
 
279
279
  // ../sdk/src/db.ts
280
+ function scopeParams(s) {
281
+ return { scope: s.scope, role: s.role };
282
+ }
283
+ function scopeLabel(s) {
284
+ if (s.scope === "user") return "db.user";
285
+ if (s.scope === "role") return `db.role(${q(s.role ?? "")})`;
286
+ return "db.shared";
287
+ }
280
288
  var Query = class {
281
- constructor(collection) {
289
+ constructor(collection, scope) {
282
290
  this.collection = collection;
291
+ this.scope = scope;
283
292
  this._where = [];
284
293
  }
285
294
  where(field, op, value) {
@@ -309,11 +318,12 @@
309
318
  prefix: this._prefix,
310
319
  updated_since: this._since,
311
320
  updated_before: this._before,
321
+ ...scopeParams(this.scope),
312
322
  ...extra
313
323
  };
314
324
  }
315
325
  label(method) {
316
- return `db.collection(${q(this.collection)}).${method}`;
326
+ return `${scopeLabel(this.scope)}.collection(${q(this.collection)}).${method}`;
317
327
  }
318
328
  async fetchPage(page, size) {
319
329
  const resp = await call("GET", `/kv/${encPath(this.collection)}`, {
@@ -340,16 +350,20 @@
340
350
  }
341
351
  };
342
352
  var Collection = class {
343
- constructor(name) {
353
+ constructor(name, scope) {
344
354
  this.name = name;
355
+ this.scope = scope;
345
356
  }
346
357
  label(method) {
347
- return `db.collection(${q(this.name)}).${method}`;
358
+ return `${scopeLabel(this.scope)}.collection(${q(this.name)}).${method}`;
348
359
  }
349
360
  /** The stored value for ``key``, or null if absent. */
350
361
  get(key) {
351
362
  return track("db", this.label(`get(${q(key)})`), async () => {
352
- const record = await getOrNull(`/kv/${encPath(this.name)}/${encPath(key)}`);
363
+ const record = await getOrNull(
364
+ `/kv/${encPath(this.name)}/${encPath(key)}`,
365
+ scopeParams(this.scope)
366
+ );
353
367
  return record ? record.value : null;
354
368
  });
355
369
  }
@@ -357,7 +371,8 @@
357
371
  put(key, value) {
358
372
  return track("db", this.label(`put(${q(key)}, ${shorten(JSON.stringify(value), 40)})`), async () => {
359
373
  const record = await call("PUT", `/kv/${encPath(this.name)}/${encPath(key)}`, {
360
- body: { value }
374
+ body: { value },
375
+ query: scopeParams(this.scope)
361
376
  });
362
377
  return record.value;
363
378
  });
@@ -366,18 +381,22 @@
366
381
  return track(
367
382
  "db",
368
383
  this.label(`delete(${q(key)})`),
369
- () => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}`)
384
+ () => call("DELETE", `/kv/${encPath(this.name)}/${encPath(key)}`, {
385
+ query: scopeParams(this.scope)
386
+ })
370
387
  );
371
388
  }
372
389
  /** All records in the collection (first page). */
373
390
  list() {
374
391
  return track("db", this.label("list()"), async () => {
375
- const resp = await call("GET", `/kv/${encPath(this.name)}`);
392
+ const resp = await call("GET", `/kv/${encPath(this.name)}`, {
393
+ query: scopeParams(this.scope)
394
+ });
376
395
  return resp.items;
377
396
  });
378
397
  }
379
398
  query() {
380
- return new Query(this.name);
399
+ return new Query(this.name, this.scope);
381
400
  }
382
401
  where(field, op, value) {
383
402
  return this.query().where(field, op, value);
@@ -386,8 +405,15 @@
386
405
  return this.query().prefix(value);
387
406
  }
388
407
  };
408
+ function namespace2(scope) {
409
+ return { collection: (name) => new Collection(name, scope) };
410
+ }
411
+ var shared = namespace2({});
389
412
  var db = {
390
- collection: (name) => new Collection(name)
413
+ shared,
414
+ user: namespace2({ scope: "user" }),
415
+ role: (roleUuid) => namespace2({ scope: "role", role: roleUuid }),
416
+ collection: shared.collection
391
417
  };
392
418
 
393
419
  // ../sdk/src/design-system.ts
@@ -423,18 +449,41 @@
423
449
  var email = { send: send2 };
424
450
 
425
451
  // ../sdk/src/files.ts
452
+ var URL_EXPIRY_MARGIN_MS = 15e3;
453
+ var urlCache = /* @__PURE__ */ new Map();
454
+ function scopeParams2(s) {
455
+ return { scope: s.scope, role: s.role };
456
+ }
457
+ function scopeKey(s) {
458
+ if (s.scope === "user") return "user";
459
+ if (s.scope === "role") return `role:${s.role ?? ""}`;
460
+ return "shared";
461
+ }
462
+ function scopeLabel2(s) {
463
+ if (s.scope === "user") return "files.user";
464
+ if (s.scope === "role") return `files.role(${q(s.role ?? "")})`;
465
+ return "files.shared";
466
+ }
467
+ function cacheKey(s, name) {
468
+ return `${scopeKey(s)} ${name}`;
469
+ }
426
470
  function contentTypeOf(data2, explicit) {
427
471
  if (explicit) return explicit;
428
472
  if (data2 instanceof Blob && data2.type) return data2.type;
429
473
  return "application/octet-stream";
430
474
  }
431
- function upload(name, data2, contentType) {
432
- return track("files", `files.upload(${q(name)}, <blob>)`, () => doUpload(name, data2, contentType));
475
+ function upload(scope, name, data2, contentType) {
476
+ return track("files", `${scopeLabel2(scope)}.upload(${q(name)}, <blob>)`, async () => {
477
+ const result = await doUpload(scope, name, data2, contentType);
478
+ urlCache.delete(cacheKey(scope, name));
479
+ return result;
480
+ });
433
481
  }
434
- async function doUpload(name, data2, contentType) {
482
+ async function doUpload(scope, name, data2, contentType) {
435
483
  const ct = contentTypeOf(data2, contentType);
436
484
  const target2 = await call("POST", "/files", {
437
- body: { name, content_type: ct }
485
+ body: { name, content_type: ct },
486
+ query: scopeParams2(scope)
438
487
  });
439
488
  const headers = { "Content-Type": ct, ...target2.headers };
440
489
  if (target2.mode === "proxy") {
@@ -449,21 +498,79 @@
449
498
  }
450
499
  const resp = await fetch(target2.url, { method: target2.method, body: data2, headers });
451
500
  if (!resp.ok) throw new Error(`upload failed: ${resp.status}`);
452
- return call("POST", "/files/finalize", { body: { name } });
501
+ return call("POST", "/files/finalize", { body: { name }, query: scopeParams2(scope) });
453
502
  }
454
- function url(name) {
455
- return buildUrl(`/files/${encPath(name)}`);
503
+ function url(scope, name) {
504
+ return buildUrl(`/files/${encPath(name)}`, scopeParams2(scope));
456
505
  }
457
- var list = () => track("files", "files.list()", async () => {
458
- const resp = await call("GET", "/files");
459
- return resp.items;
460
- });
461
- var remove = (name) => track("files", `files.delete(${q(name)})`, () => call("DELETE", `/files/${encPath(name)}`));
462
- var files = { upload, url, list, delete: remove };
506
+ async function urls(scope, names) {
507
+ return track("files", `${scopeLabel2(scope)}.urls(<${names.length} names>)`, async () => {
508
+ const unique = [...new Set(names)];
509
+ const now = Date.now();
510
+ const items = /* @__PURE__ */ new Map();
511
+ const unresolved = [];
512
+ for (const name of unique) {
513
+ const cached = urlCache.get(cacheKey(scope, name));
514
+ if (cached && cached.expiresAt > now + URL_EXPIRY_MARGIN_MS) items.set(name, cached.item);
515
+ else {
516
+ urlCache.delete(cacheKey(scope, name));
517
+ unresolved.push(name);
518
+ }
519
+ }
520
+ let missing = [];
521
+ if (unresolved.length) {
522
+ const resolved = await call("POST", "/files/urls", {
523
+ body: { names: unresolved },
524
+ query: scopeParams2(scope)
525
+ });
526
+ missing = resolved.missing;
527
+ for (const item of resolved.items) {
528
+ const normalized = { ...item, url: new URL(item.url, location.origin).toString() };
529
+ const expiresAt = item.expires_in === null ? Number.POSITIVE_INFINITY : now + item.expires_in * 1e3;
530
+ urlCache.set(cacheKey(scope, item.name), { item: normalized, expiresAt });
531
+ items.set(item.name, normalized);
532
+ }
533
+ }
534
+ return { items: unique.flatMap((name) => items.has(name) ? [items.get(name)] : []), missing };
535
+ });
536
+ }
537
+ function list(scope) {
538
+ return track("files", `${scopeLabel2(scope)}.list()`, async () => {
539
+ const resp = await call("GET", "/files", { query: scopeParams2(scope) });
540
+ return resp.items;
541
+ });
542
+ }
543
+ function remove(scope, name) {
544
+ return track("files", `${scopeLabel2(scope)}.delete(${q(name)})`, async () => {
545
+ await call("DELETE", `/files/${encPath(name)}`, { query: scopeParams2(scope) });
546
+ urlCache.delete(cacheKey(scope, name));
547
+ });
548
+ }
549
+ function fileNamespace(scope) {
550
+ return {
551
+ upload: (name, data2, contentType) => upload(scope, name, data2, contentType),
552
+ url: (name) => url(scope, name),
553
+ urls: (names) => urls(scope, names),
554
+ list: () => list(scope),
555
+ delete: (name) => remove(scope, name)
556
+ };
557
+ }
558
+ var shared2 = fileNamespace({});
559
+ var files = {
560
+ shared: shared2,
561
+ user: fileNamespace({ scope: "user" }),
562
+ role: (roleUuid) => fileNamespace({ scope: "role", role: roleUuid }),
563
+ upload: shared2.upload,
564
+ url: shared2.url,
565
+ urls: shared2.urls,
566
+ list: shared2.list,
567
+ delete: shared2.delete
568
+ };
463
569
 
464
570
  // ../sdk/src/identity.ts
465
571
  var me = () => track("me", "me()", () => call("GET", "/me"));
466
572
  var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
573
+ var roles = () => track("roles", "roles()", () => call("GET", "/roles"));
467
574
 
468
575
  // ../sdk/src/llm.ts
469
576
  function label2(method, input, opts = {}) {
@@ -589,6 +696,7 @@
589
696
  target.files = files;
590
697
  target.me = me;
591
698
  target.appUsers = appUsers;
699
+ target.roles = roles;
592
700
  target.designSystem = designSystem;
593
701
  target.email = email;
594
702
  target.llm = llm;
@@ -596,7 +704,7 @@
596
704
  target.data = data;
597
705
  target.postgres = postgres;
598
706
  target.bigquery = bigquery;
599
- target.snowflake = snowflake;
707
+ target.turso = turso;
600
708
  target.dataConnectors = dataConnectors;
601
709
  target.connector = connector;
602
710
  target.serviceConnectors = serviceConnectors;