@abloatai/ablo 0.11.0 → 0.11.2

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 (75) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +72 -25
  3. package/dist/Model.d.ts +39 -0
  4. package/dist/Model.js +68 -0
  5. package/dist/auth/credentialPolicy.d.ts +145 -0
  6. package/dist/auth/credentialPolicy.js +130 -0
  7. package/dist/cli.cjs +154 -25
  8. package/dist/client/Ablo.d.ts +39 -88
  9. package/dist/client/Ablo.js +54 -99
  10. package/dist/client/ApiClient.d.ts +10 -1
  11. package/dist/client/ApiClient.js +23 -12
  12. package/dist/client/auth.d.ts +21 -9
  13. package/dist/client/auth.js +42 -6
  14. package/dist/client/createModelProxy.d.ts +74 -10
  15. package/dist/client/createModelProxy.js +85 -4
  16. package/dist/client/httpClient.d.ts +17 -3
  17. package/dist/client/httpClient.js +1 -0
  18. package/dist/client/identity.js +134 -122
  19. package/dist/client/index.d.ts +1 -1
  20. package/dist/client/sessionMint.d.ts +15 -0
  21. package/dist/client/sessionMint.js +86 -0
  22. package/dist/errorCodes.d.ts +2 -0
  23. package/dist/errorCodes.js +3 -1
  24. package/dist/errors.d.ts +3 -2
  25. package/dist/errors.js +3 -2
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +4 -7
  28. package/dist/mutators/RecordingTransaction.js +14 -42
  29. package/dist/react/AbloProvider.d.ts +1 -6
  30. package/dist/react/AbloProvider.js +1 -5
  31. package/dist/react/context.d.ts +1 -31
  32. package/dist/react/context.js +2 -2
  33. package/dist/react/index.d.ts +0 -6
  34. package/dist/react/index.js +0 -7
  35. package/dist/react/useSyncStatus.d.ts +1 -1
  36. package/dist/realtime/index.d.ts +1 -1
  37. package/dist/schema/generate.js +1 -2
  38. package/dist/schema/schema.d.ts +16 -5
  39. package/dist/schema/schema.js +26 -0
  40. package/dist/surface.d.ts +29 -0
  41. package/dist/surface.js +60 -0
  42. package/dist/sync/ConnectionManager.d.ts +16 -5
  43. package/dist/sync/ConnectionManager.js +42 -7
  44. package/dist/transactions/TransactionQueue.js +22 -10
  45. package/dist/types/global.d.ts +11 -3
  46. package/dist/types/global.js +8 -3
  47. package/dist/types/streams.d.ts +0 -22
  48. package/dist/utils/mobx-setup.js +1 -0
  49. package/docs/api-keys.md +49 -0
  50. package/docs/api.md +6 -5
  51. package/docs/client-behavior.md +7 -3
  52. package/docs/coordination.md +88 -24
  53. package/docs/data-sources.md +29 -9
  54. package/docs/examples/existing-python-backend.md +9 -5
  55. package/docs/examples/scoped-agent.md +1 -1
  56. package/docs/guarantees.md +4 -3
  57. package/docs/identity.md +89 -82
  58. package/docs/integration-guide.md +19 -10
  59. package/docs/migration.md +49 -2
  60. package/docs/quickstart.md +65 -33
  61. package/docs/react.md +49 -3
  62. package/docs/schema-contract.md +23 -5
  63. package/llms-full.txt +43 -24
  64. package/llms.txt +17 -15
  65. package/package.json +1 -1
  66. package/dist/api/index.d.ts +0 -10
  67. package/dist/api/index.js +0 -9
  68. package/dist/principal.d.ts +0 -44
  69. package/dist/principal.js +0 -49
  70. package/dist/react/SyncGroupProvider.d.ts +0 -19
  71. package/dist/react/SyncGroupProvider.js +0 -44
  72. package/dist/react/useClaim.d.ts +0 -29
  73. package/dist/react/useClaim.js +0 -42
  74. package/dist/react/usePresence.d.ts +0 -32
  75. package/dist/react/usePresence.js +0 -41
package/dist/cli.cjs CHANGED
@@ -276803,7 +276803,8 @@ var ERROR_CODES = {
276803
276803
  malformed_subscription: wire("validation", 400, false, "The update_subscription payload was malformed; expected { syncGroups: string[] }."),
276804
276804
  model_claimed: wire("claim", 409, false, "The model instance is claimed by another participant."),
276805
276805
  model_claimed_timeout: wire("claim", 409, false, "Timed out waiting for a model claim to clear."),
276806
- model_claim_not_configured: client("claim", "Claiming was requested on a model that has no claim configuration."),
276806
+ model_claim_not_configured: client("claim", "Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically \u2014 there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path)."),
276807
+ model_watch_not_configured: client("claim", "watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport)."),
276807
276808
  // ── stale context / idempotency (409) ──────────────────────────────
276808
276809
  stale_context: wire("conflict", 409, true, "The write carried a readAt watermark that is now stale; re-read and retry."),
276809
276810
  idempotency_conflict: wire("conflict", 409, false, "The same Idempotency-Key was reused with a different request body."),
@@ -276855,6 +276856,7 @@ var ERROR_CODES = {
276855
276856
  schema_scope_kind_invalid: wire("schema", 400, false, "A scope kind in the schema is invalid."),
276856
276857
  schema_field_not_camelcase: wire("schema", 400, false, "A schema field name is not camelCase."),
276857
276858
  schema_field_consecutive_caps: wire("schema", 400, false, "A schema field name has consecutive capital letters."),
276859
+ schema_reserved_field: client("schema", "A model redeclared a reserved base field (id, createdAt, updatedAt, organizationId, createdBy) that the SDK provides automatically."),
276858
276860
  schema_grants_shape_invalid: wire("schema", 400, false, "A grants declaration has an invalid shape."),
276859
276861
  schema_grants_identifier_unsafe: wire("schema", 400, false, "A grants declaration referenced an unsafe identifier."),
276860
276862
  schema_grants_relation_kind: wire("schema", 400, false, "A grants relation referenced an invalid kind."),
@@ -279616,6 +279618,23 @@ var import_source = require("@abloatai/ablo/source");
279616
279618
  // src/cli/push.ts
279617
279619
  init_cjs_shims();
279618
279620
  var import_picocolors3 = __toESM(require_picocolors(), 1);
279621
+
279622
+ // src/auth/credentialPolicy.ts
279623
+ init_cjs_shims();
279624
+ var KIND_BY_PREFIX = [
279625
+ ["sk_", "secret"],
279626
+ ["ek_", "ephemeral"],
279627
+ ["rk_", "restricted"],
279628
+ ["pk_", "publishable"]
279629
+ ];
279630
+ function classifyCredentialKind(value) {
279631
+ for (const [prefix, kind] of KIND_BY_PREFIX) {
279632
+ if (value.startsWith(prefix)) return kind;
279633
+ }
279634
+ return null;
279635
+ }
279636
+
279637
+ // src/cli/push.ts
279619
279638
  var import_fs4 = require("fs");
279620
279639
  var import_path3 = require("path");
279621
279640
  var import_schema2 = require("@abloatai/ablo/schema");
@@ -279928,7 +279947,7 @@ async function push(argv) {
279928
279947
  console.error(import_picocolors3.default.dim(` Re-push with ${import_picocolors3.default.bold("--force")} to override, or use ${import_picocolors3.default.bold("--rename old:new")} if you renamed a model.`));
279929
279948
  } else if (status2 === 403) {
279930
279949
  console.error(import_picocolors3.default.red(` Forbidden: ${body.message ?? body.reason ?? "key lacks schema:push scope"}`));
279931
- if (args.apiKey?.startsWith("rk_")) {
279950
+ if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
279932
279951
  console.error(
279933
279952
  import_picocolors3.default.dim(
279934
279953
  ` Schema pushes need a SECRET key: ${import_picocolors3.default.bold("sk_test_")} (sandbox dev loop) or a dashboard ${import_picocolors3.default.bold("sk_live_")} (production deploy: ${import_picocolors3.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")}).`
@@ -279942,6 +279961,14 @@ async function push(argv) {
279942
279961
  }
279943
279962
 
279944
279963
  // src/cli/migrate.ts
279964
+ var MIGRATE_USAGE = ` ablo migrate \u2014 provision your schema's tables in your own Postgres (DATABASE_URL)
279965
+
279966
+ Usage:
279967
+ npx ablo migrate Create the synced-model tables (with row-level security)
279968
+ npx ablo migrate --dry-run Print the SQL without executing it
279969
+ npx ablo migrate --output schema.sql Write the SQL to a file instead of applying
279970
+ npx ablo migrate --schema <path> Use a schema file other than ablo/schema.ts
279971
+ npx ablo migrate --export <name> Use a named export other than \`schema\``;
279945
279972
  var DEFAULT_SCHEMA_PATH2 = "ablo/schema.ts";
279946
279973
  var DEFAULT_EXPORT2 = "schema";
279947
279974
  function parseMigrateArgs(argv) {
@@ -280215,7 +280242,7 @@ function classifyKey(apiKey, activeMode) {
280215
280242
  reason: `Production schema deploys run one-shot: ${import_picocolors6.default.bold("ABLO_API_KEY=sk_live_\u2026 npx ablo push")} (or ${import_picocolors6.default.bold("ablo mode production")}). ${import_picocolors6.default.bold("--watch")} is sandbox-only.`
280216
280243
  };
280217
280244
  }
280218
- if (apiKey.startsWith("rk_")) {
280245
+ if (classifyCredentialKind(apiKey) === "restricted") {
280219
280246
  return {
280220
280247
  ok: false,
280221
280248
  reason: `Restricted (${import_picocolors6.default.bold("rk_")}) keys can't push schema. Use a secret key: ${import_picocolors6.default.bold("sk_test_")} for the sandbox dev loop, or ${import_picocolors6.default.bold("sk_live_")} with ${import_picocolors6.default.bold("npx ablo push")} for a production deploy.`
@@ -280478,7 +280505,10 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280478
280505
  s.message("Provisioning a sandbox key\u2026");
280479
280506
  const provRes = await fetch(`${AUTH_URL}/api/cli/provision-key`, {
280480
280507
  method: "POST",
280481
- headers: { authorization: `Bearer ${accessToken}` }
280508
+ headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
280509
+ // Pass the device_code so the server can scope the minted keys to the
280510
+ // project the user picked at /cli (login project picker). Harmless if none.
280511
+ body: JSON.stringify({ device_code: code.device_code })
280482
280512
  }).catch(() => null);
280483
280513
  if (!provRes || !provRes.ok) {
280484
280514
  s.stop("Could not provision a key.");
@@ -280706,6 +280736,39 @@ async function projects(argv) {
280706
280736
  );
280707
280737
  return;
280708
280738
  }
280739
+ if (sub === "rename") {
280740
+ const ref = argv[1];
280741
+ const name = argv.slice(2).join(" ").trim();
280742
+ if (!ref || ref.startsWith("-") || !name) {
280743
+ console.error(import_picocolors9.default.red(" usage: ablo projects rename <slug|id> <new name>"));
280744
+ process.exit(1);
280745
+ }
280746
+ const all = await fetchProjects();
280747
+ const target = all.find((p2) => p2.slug === ref || p2.id === ref);
280748
+ if (!target) {
280749
+ console.error(import_picocolors9.default.red(` No project "${ref}".`) + import_picocolors9.default.dim(" Run ablo projects list."));
280750
+ process.exit(1);
280751
+ }
280752
+ if (target.default) {
280753
+ console.error(import_picocolors9.default.red(" The default project cannot be renamed."));
280754
+ process.exit(1);
280755
+ }
280756
+ const { status: status2, body } = await request(`/api/v1/projects/${target.id}`, requireKey(), {
280757
+ method: "PATCH",
280758
+ body: { name }
280759
+ });
280760
+ if (status2 !== 200) {
280761
+ console.error(
280762
+ import_picocolors9.default.red(` Rename failed (${status2}): ${String(body.message ?? body.code ?? "")}`)
280763
+ );
280764
+ process.exit(1);
280765
+ }
280766
+ const updated = body;
280767
+ console.log(
280768
+ ` ${import_picocolors9.default.green("\u2713")} Renamed ${import_picocolors9.default.bold(updated.slug)} \u2192 ${import_picocolors9.default.bold(updated.name ?? updated.slug)} ${import_picocolors9.default.dim(`(${updated.id})`)}`
280769
+ );
280770
+ return;
280771
+ }
280709
280772
  if (sub === "use") {
280710
280773
  const ref = argv[1];
280711
280774
  if (!ref) {
@@ -280733,7 +280796,9 @@ async function projects(argv) {
280733
280796
  return;
280734
280797
  }
280735
280798
  console.error(
280736
- import_picocolors9.default.red(` unknown subcommand: ${sub}`) + import_picocolors9.default.dim(` (expected ${import_picocolors9.default.bold("list")}, ${import_picocolors9.default.bold("create")}, or ${import_picocolors9.default.bold("use")})`)
280799
+ import_picocolors9.default.red(` unknown subcommand: ${sub}`) + import_picocolors9.default.dim(
280800
+ ` (expected ${import_picocolors9.default.bold("list")}, ${import_picocolors9.default.bold("create")}, ${import_picocolors9.default.bold("rename")}, or ${import_picocolors9.default.bold("use")})`
280801
+ )
280737
280802
  );
280738
280803
  process.exit(1);
280739
280804
  }
@@ -281006,7 +281071,7 @@ function requireKey2(mode2) {
281006
281071
  );
281007
281072
  process.exit(1);
281008
281073
  }
281009
- if (!apiKey.startsWith("sk_")) {
281074
+ if (classifyCredentialKind(apiKey) !== "secret") {
281010
281075
  console.error(import_picocolors12.default.red(" Managing webhooks requires a secret key ") + import_picocolors12.default.dim("(sk_test_ / sk_live_)."));
281011
281076
  process.exit(1);
281012
281077
  }
@@ -282130,8 +282195,18 @@ async function drizzlePull(argv) {
282130
282195
  var LOGO = `
282131
282196
  ${brand("ablo")} ${import_picocolors18.default.dim("sync engine")}
282132
282197
  `;
282198
+ var SUBCOMMAND_USAGE = {
282199
+ migrate: MIGRATE_USAGE
282200
+ };
282133
282201
  async function main() {
282134
- const command = process.argv[2];
282202
+ let command = process.argv[2];
282203
+ if (command && process.argv.slice(3).some((a) => a === "--help" || a === "-h")) {
282204
+ if (SUBCOMMAND_USAGE[command]) {
282205
+ console.log(SUBCOMMAND_USAGE[command]);
282206
+ return;
282207
+ }
282208
+ command = void 0;
282209
+ }
282135
282210
  if (command === "init") {
282136
282211
  await init(process.argv.slice(3));
282137
282212
  } else if (command === "login") {
@@ -282149,8 +282224,14 @@ async function main() {
282149
282224
  } else if (command === "webhooks") {
282150
282225
  await webhooks(process.argv.slice(3));
282151
282226
  } else if (command === "dev") {
282152
- console.log(import_picocolors18.default.dim(" `ablo dev` is now `ablo push --watch` \u2014 running that."));
282153
- await dev([...process.argv.slice(3), "--watch"]);
282227
+ const devArgs = process.argv.slice(3);
282228
+ const oneShot = devArgs.includes("--no-watch");
282229
+ console.log(
282230
+ import_picocolors18.default.dim(
282231
+ oneShot ? " `ablo dev --no-watch` is `ablo push` (push once, no watcher) \u2014 running that." : " `ablo dev` is now `ablo push --watch` \u2014 running that."
282232
+ )
282233
+ );
282234
+ await dev(oneShot ? devArgs : [...devArgs, "--watch"]);
282154
282235
  } else if (command === "check") {
282155
282236
  await check(process.argv.slice(3));
282156
282237
  } else if (command === "pull") {
@@ -282208,6 +282289,8 @@ async function main() {
282208
282289
  console.log(` npx ablo pull prisma [path] Generate schema.ts from a Prisma schema (keeps enums + relations)`);
282209
282290
  console.log(` npx ablo pull drizzle <module> Generate schema.ts from a Drizzle schema (keeps enums + relations)`);
282210
282291
  console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
282292
+ console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (DATABASE_URL)`);
282293
+ console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
282211
282294
  console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
282212
282295
  console.log(` npx ablo push --force Allow destructive/unexecutable changes`);
282213
282296
  console.log(` npx ablo push --rename a:b Treat model "a" as renamed to "b"`);
@@ -282285,6 +282368,10 @@ function detectOrm(override) {
282285
282368
  }
282286
282369
  return "none";
282287
282370
  }
282371
+ function detectNextLayout() {
282372
+ const useSrc = (0, import_fs12.existsSync)((0, import_path7.join)("src", "app")) || !(0, import_fs12.existsSync)("app") && (0, import_fs12.existsSync)("src");
282373
+ return useSrc ? { appBase: (0, import_path7.join)("src", "app"), aliasBase: "src" } : { appBase: "app", aliasBase: "." };
282374
+ }
282288
282375
  async function chooseOption(name, flagValue, fallback, allowed, interactive, prompt) {
282289
282376
  if (flagValue !== void 0) {
282290
282377
  if (!allowed.includes(flagValue)) {
@@ -282384,7 +282471,8 @@ async function init(args = []) {
282384
282471
  "Non-interactive (no TTY / --yes)"
282385
282472
  );
282386
282473
  }
282387
- const abloDir = "ablo";
282474
+ const layout = framework === "nextjs" ? detectNextLayout() : { appBase: "app", aliasBase: "." };
282475
+ const abloDir = (0, import_path7.join)(layout.aliasBase, "ablo");
282388
282476
  (0, import_fs12.mkdirSync)(abloDir, { recursive: true });
282389
282477
  const created = [];
282390
282478
  let schemaSource = generateSchema();
@@ -282415,41 +282503,51 @@ async function init(args = []) {
282415
282503
  created.push(`${abloDir}/schema.ts${schemaNote}`);
282416
282504
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "index.ts"), generateSyncConfig(auth, storage));
282417
282505
  created.push(`${abloDir}/index.ts`);
282506
+ (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "register.ts"), generateRegister());
282507
+ created.push(`${abloDir}/register.ts`);
282418
282508
  const orm = detectOrm(opts.orm);
282419
282509
  if (storage === "endpoint") {
282420
282510
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "data-source.ts"), generateDataSource(orm));
282421
282511
  created.push(`${abloDir}/data-source.ts${orm === "drizzle" ? " (Drizzle)" : " (Prisma)"}`);
282422
282512
  }
282423
282513
  const envFile = framework === "nextjs" ? ".env.local" : ".env";
282514
+ const resolvedKey = process.env.ABLO_API_KEY ? void 0 : resolveApiKey("sandbox");
282515
+ const wireRealKey = envFile === ".env.local" && Boolean(resolvedKey);
282516
+ const envBody = generateEnv(storage, { includeApiKey: !wireRealKey });
282424
282517
  if (!(0, import_fs12.existsSync)(envFile)) {
282425
- (0, import_fs12.writeFileSync)(envFile, generateEnv(storage));
282518
+ (0, import_fs12.writeFileSync)(envFile, envBody);
282426
282519
  created.push(envFile);
282427
282520
  } else {
282428
282521
  const existing = (0, import_fs12.readFileSync)(envFile, "utf-8");
282429
282522
  if (!existing.includes("ABLO_")) {
282430
- (0, import_fs12.writeFileSync)(envFile, existing + "\n" + generateEnv(storage));
282523
+ (0, import_fs12.writeFileSync)(envFile, existing + "\n" + envBody);
282431
282524
  created.push(`${envFile} ${import_picocolors18.default.dim("(appended)")}`);
282432
282525
  } else {
282433
282526
  created.push(`${envFile} ${import_picocolors18.default.dim("(already configured)")}`);
282434
282527
  }
282435
282528
  }
282529
+ if (wireRealKey && resolvedKey) {
282530
+ wireEnvLocal(resolvedKey);
282531
+ created.push(`.env.local ${import_picocolors18.default.dim("(ABLO_API_KEY set from your login)")}`);
282532
+ }
282436
282533
  if (agent) {
282437
282534
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "agent.ts"), generateAgent());
282438
282535
  created.push(`${abloDir}/agent.ts`);
282439
282536
  }
282440
282537
  if (framework === "nextjs") {
282441
282538
  if (storage === "endpoint") {
282442
- const webhookDir = (0, import_path7.join)("app", "api", "ablo", "webhooks");
282539
+ const webhookDir = (0, import_path7.join)(layout.appBase, "api", "ablo", "webhooks");
282443
282540
  (0, import_fs12.mkdirSync)(webhookDir, { recursive: true });
282444
282541
  (0, import_fs12.writeFileSync)((0, import_path7.join)(webhookDir, "route.ts"), generateWebhookRoute(orm));
282445
282542
  created.push(`${webhookDir}/route.ts${orm === "prisma" ? " (Prisma mirror)" : " (add your database write)"}`);
282446
282543
  }
282447
- (0, import_fs12.writeFileSync)((0, import_path7.join)("app", "providers.tsx"), generateProviders());
282448
- created.push(`app/providers.tsx ${import_picocolors18.default.dim("(wrap app/layout.tsx in <Providers>)")}`);
282449
- const sessionDir = (0, import_path7.join)("app", "api", "ablo-session");
282544
+ const providersPath = (0, import_path7.join)(layout.appBase, "providers.tsx");
282545
+ (0, import_fs12.writeFileSync)(providersPath, generateProviders());
282546
+ created.push(`${providersPath} ${import_picocolors18.default.dim(`(wrap ${(0, import_path7.join)(layout.appBase, "layout.tsx")} in <Providers>)`)}`);
282547
+ const sessionDir = (0, import_path7.join)(layout.appBase, "api", "ablo-session");
282450
282548
  (0, import_fs12.mkdirSync)(sessionDir, { recursive: true });
282451
282549
  (0, import_fs12.writeFileSync)((0, import_path7.join)(sessionDir, "route.ts"), generateSessionRoute());
282452
- created.push(`app/api/ablo-session/route.ts ${import_picocolors18.default.dim("(wire your auth)")}`);
282550
+ created.push(`${(0, import_path7.join)(sessionDir, "route.ts")} ${import_picocolors18.default.dim("(wire your auth)")}`);
282453
282551
  }
282454
282552
  if (framework !== "vanilla") {
282455
282553
  (0, import_fs12.writeFileSync)((0, import_path7.join)(abloDir, "TaskList.tsx"), generateComponent());
@@ -282478,7 +282576,7 @@ async function init(args = []) {
282478
282576
  `Provision your DB: ${import_picocolors18.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors18.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors18.default.bold("/api/ablo/source")}`
282479
282577
  ],
282480
282578
  ...framework === "nextjs" ? [
282481
- `Wrap ${import_picocolors18.default.bold("app/layout.tsx")} in ${import_picocolors18.default.bold("<Providers>")} (app/providers.tsx) and add your auth to ${import_picocolors18.default.bold("app/api/ablo-session/route.ts")}`
282579
+ `Wrap ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "layout.tsx"))} in ${import_picocolors18.default.bold("<Providers>")} (${(0, import_path7.join)(layout.appBase, "providers.tsx")}) and add your auth to ${import_picocolors18.default.bold((0, import_path7.join)(layout.appBase, "api", "ablo-session", "route.ts"))}`
282482
282580
  ] : [],
282483
282581
  `Run ${import_picocolors18.default.bold(`${pm} run dev`)} and open two browser tabs \u2014 changes sync in real-time`,
282484
282582
  ...agent ? [
@@ -282557,14 +282655,31 @@ export const sync = Ablo({
282557
282655
  apiKey: process.env.ABLO_API_KEY,${databaseLine}${authLine}
282558
282656
  schema,
282559
282657
  });
282658
+
282659
+ // Name the client's type off the constructed value \u2014 the overload resolves at
282660
+ // this call site, so this carries the full typed surface. (Like tRPC's
282661
+ // \`typeof appRouter\`, Drizzle's \`typeof db\`.) Prefer this over \`ReturnType<typeof Ablo>\`.
282662
+ export type Sync = typeof sync;
282663
+ `;
282664
+ }
282665
+ function generateRegister() {
282666
+ return `import type { schema } from './schema';
282667
+
282668
+ declare module '@abloatai/ablo' {
282669
+ interface Register {
282670
+ Schema: typeof schema;
282671
+ }
282672
+ }
282673
+
282674
+ export {};
282560
282675
  `;
282561
282676
  }
282562
- function generateEnv(storage) {
282677
+ function generateEnv(storage, opts = {}) {
282678
+ const { includeApiKey = true } = opts;
282563
282679
  const databaseBlock = storage === "direct" ? "# Your Postgres \u2014 the system of record. The client registers this connection\n# (sent once over TLS, stored sealed) and every row lives HERE, never with Ablo.\n# Use a dedicated non-superuser role; the browser never sees this value.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
282564
282680
  const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
282565
- return `# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (\`npx ablo dev\`)
282566
- ABLO_API_KEY=sk_test_your_key_here
282567
- ${webhookBlock}${databaseBlock}`;
282681
+ const apiKeyBlock = includeApiKey ? "# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (`npx ablo push`)\nABLO_API_KEY=sk_test_your_key_here\n" : "";
282682
+ return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
282568
282683
  }
282569
282684
  function generateDataSource(orm) {
282570
282685
  return orm === "drizzle" ? drizzleDataSourceScaffold() : prismaDataSourceScaffold();
@@ -282826,9 +282941,23 @@ import Ablo from '@abloatai/ablo';
282826
282941
  import { AbloProvider } from '@abloatai/ablo/react';
282827
282942
  import { schema } from '@/ablo/schema';
282828
282943
 
282829
- // The browser client holds NO secret. \`authEndpoint\` points at the route below,
282830
- // which mints a short-lived session token (already scoped to the org + user).
282831
- const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
282944
+ // The browser client holds NO secret. The \`apiKey\` resolver fetches the route
282945
+ // below, which mints a short-lived session token (already scoped to the org +
282946
+ // user); the client keeps it fresh (refresh timer + wake/online/focus re-mint).
282947
+ // Contract: return the token, return \`null\` when the user is signed out
282948
+ // (\u2192 the client signs out), or throw on a transient failure (\u2192 it retries).
282949
+ const ablo = Ablo({
282950
+ schema,
282951
+ apiKey: async () => {
282952
+ const res = await fetch('/api/ablo-session', {
282953
+ method: 'POST',
282954
+ credentials: 'include',
282955
+ });
282956
+ if (!res.ok) return null;
282957
+ const { token } = (await res.json()) as { token: string | null };
282958
+ return token;
282959
+ },
282960
+ });
282832
282961
 
282833
282962
  export function Providers({ children }: { children: React.ReactNode }) {
282834
282963
  return <AbloProvider client={ablo}>{children}</AbloProvider>;
@@ -28,7 +28,6 @@ import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
28
28
  import type { SyncGroupInput } from '../schema/roles.js';
29
29
  import { type SyncStatus } from '../BaseSyncedStore.js';
30
30
  import type { ClaimStream, ClaimWaitOptions, PresenceStream, Snapshot } from '../types/streams.js';
31
- import type { ParticipantManager } from '../sync/participants.js';
32
31
  import type { ClaimHandle, Duration, Claim } from '../types/streams.js';
33
32
  import { type AbloApi, type AbloApiClientOptions, type AbloApiClaims } from './ApiClient.js';
34
33
  import { type AbloHttpClient, type AbloHttpClientOptions } from './httpClient.js';
@@ -77,35 +76,24 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
77
76
  * usually pass nothing). A long-lived key needs no refresh; the client uses
78
77
  * it as-is.
79
78
  *
80
- * Accepts a static string or an async `() => Promise<string>` resolver if you
81
- * rotate keys out-of-band (resolved at bootstrap).
79
+ * Accepts a static string OR an async `() => Promise<string | null>` resolver
80
+ * the single credential path. Use the resolver form for two cases:
82
81
  *
83
- * Browser apps that mint a SHORT-LIVED per-user key (`ek_`) from a login can't
84
- * ship a secret those use {@link getToken} (or {@link authEndpoint}) instead,
85
- * which the client refreshes for you. That's the only case that isn't "just
86
- * `apiKey`".
87
- */
88
- apiKey?: string | ApiKeySetter | null | undefined;
89
- /**
90
- * Opt-in for the SHORT-LIVED per-user browser case: an async resolver for a
91
- * fresh bearer (`ek_`/`rk_`) your backend minted for the signed-in user. The
92
- * client calls it once before connect and then keeps the key fresh for you —
93
- * a refresh timer ahead of expiry plus re-mint on OS-wake / network-online /
94
- * tab-focus, and a reactive re-mint when a probe finds the key stale. You
95
- * never call a refresh method (Supabase `autoRefreshToken` model).
82
+ * - **Key rotation** (server): pull a fresh `sk_`/`pk_` from a vault on each
83
+ * bootstrap (AWS STS, GCP IAM, Vault).
84
+ * - **Short-lived per-user browser** auth: return the fresh `ek_`/`rk_` bearer
85
+ * your backend minted for the signed-in user. The client mints once before
86
+ * connect, then keeps it fresh for you — a refresh timer ahead of expiry
87
+ * plus re-mint on OS-wake / network-online / tab-focus, and a reactive
88
+ * re-mint when a probe finds the key stale. You never call a refresh method
89
+ * (Supabase `autoRefreshToken` model).
96
90
  *
97
- * Contract: resolve a token, resolve `null` when the login itself is gone
98
- * (terminal → sign out), or THROW on a transient failure ( back off, never
99
- * sign out). Leave unset for the static-`apiKey` path.
100
- */
101
- getToken?: (() => Promise<string | null>) | undefined;
102
- /**
103
- * Convenience over {@link getToken}: a URL on YOUR backend that returns
104
- * `{ token }`. The client POSTs to it (with cookies, so it's authed by the
105
- * user's session) to mint + refresh the bearer. Ignored when `getToken` is
106
- * set. Pure sugar — `getToken: () => fetch(url).then(r => r.json()).then(b => b.token)`.
91
+ * Resolver contract: resolve a token; resolve `null` when the login itself is
92
+ * gone (terminal → the client signs out / fails `ready()` with `session_expired`);
93
+ * or THROW on a transient failure (→ back off and retry, never sign out). A
94
+ * static string never refreshes — it is used as-is.
107
95
  */
108
- authEndpoint?: string | undefined;
96
+ apiKey?: string | ApiKeySetter | null | undefined;
109
97
  /**
110
98
  * Direct-URL convenience connector: a connection string to your own Postgres
111
99
  * that Ablo can register for a dedicated tenant.
@@ -138,6 +126,9 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
138
126
  * `AbloHttpClient<S>`, so stateful-only capabilities (`get`/`getAll`,
139
127
  * `onChange`) are compile errors rather than latent runtime gaps.
140
128
  *
129
+ * Note: session/credential minting (`sessions.create`) currently runs on the
130
+ * stateful (default) client, not the http client.
131
+ *
141
132
  * @default 'websocket'
142
133
  */
143
134
  transport?: 'websocket' | 'http' | undefined;
@@ -225,18 +216,6 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
225
216
  * only for the advanced Model / Claim / Commit client.
226
217
  */
227
218
  schema: Schema<S>;
228
- /**
229
- * Short-lived-bearer resolver for the per-user browser path (mirrors the
230
- * public {@link AbloOptions.getToken}). The client mints the first token
231
- * before connect and refreshes it (timer + wake/online/focus) — see
232
- * {@link resolveCredentialResolver}.
233
- */
234
- getToken?: (() => Promise<string | null>) | undefined;
235
- /**
236
- * Backend URL returning `{ token }`; sugar over {@link getToken}. Mirrors the
237
- * public {@link AbloOptions.authEndpoint}.
238
- */
239
- authEndpoint?: string | undefined;
240
219
  /**
241
220
  * @deprecated Server derives participant kind from the apiKey's
242
221
  * scope. Pass apiKey only; this option will be removed once the
@@ -385,8 +364,8 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
385
364
  * `create({ data })` / `update({ id, data })` / `delete({ id })` — writes
386
365
  * `claim({ id })` — durable claim handle for coordinated writes
387
366
  */
388
- export type { ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './createModelProxy.js';
389
- import type { ModelOperations, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ModelLoadOptions } from './createModelProxy.js';
367
+ export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './createModelProxy.js';
368
+ import type { ModelOperations, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ServerReadOptions } from './createModelProxy.js';
390
369
  export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
391
370
  export type CommitWait = 'queued' | 'confirmed';
392
371
  export interface ModelRead<T = Record<string, unknown>> {
@@ -394,23 +373,15 @@ export interface ModelRead<T = Record<string, unknown>> {
394
373
  readonly stamp: number;
395
374
  readonly claims: readonly ModelClaim[];
396
375
  }
397
- export type IfClaimedPolicy = 'return' | 'wait' | 'fail';
376
+ export type IfClaimedPolicy = 'return' | 'fail';
398
377
  export interface ClaimedOptions {
399
378
  /**
400
- * What to do when another participant has claimed the target. `return`
401
- * includes active claim metadata in the response, `wait` resolves after the
402
- * claim clears, and `fail` throws `AbloClaimedError`.
379
+ * What to do when another participant has claimed the target: `return`
380
+ * includes active claim metadata in the response; `fail` throws
381
+ * `AbloClaimedError`. Waiting for a claim to clear is a claim-side concern —
382
+ * take `ablo.<model>.claim({ id })` (it queues fairly); reads never block.
403
383
  */
404
384
  readonly ifClaimed?: IfClaimedPolicy;
405
- /** Max time to wait for peer claims to clear, in milliseconds. */
406
- readonly claimedTimeout?: number;
407
- /** HTTP API polling interval while waiting. WebSocket clients ignore it. */
408
- readonly claimedPollInterval?: number;
409
- /**
410
- * Backpressure for `ifClaimed: 'wait'`: reject instead of waiting if the
411
- * row's FIFO line is already `>= maxQueueDepth` deep.
412
- */
413
- readonly maxQueueDepth?: number;
414
385
  }
415
386
  export type { ClaimWaitOptions } from '../types/streams.js';
416
387
  export interface ModelReadOptions extends ClaimedOptions {
@@ -527,7 +498,7 @@ export interface ModelClient<T = Record<string, unknown>> {
527
498
  * `limit`. Present on the stateless protocol client; the store-backed
528
499
  * `.model(name)` accessor omits it (use the typed `ablo.<model>.list` there).
529
500
  */
530
- list?(options?: ModelLoadOptions<T>): Promise<T[]>;
501
+ list?(options?: ServerReadOptions<T>): Promise<T[]>;
531
502
  create(params: ModelMutationOptions & {
532
503
  readonly data: Record<string, unknown>;
533
504
  readonly id?: string | null;
@@ -560,7 +531,7 @@ export interface CreateUserSessionParams {
560
531
  id: string;
561
532
  };
562
533
  /** Sync groups this session may subscribe to — typed (`'default'` or
563
- * `<namespace>:<id>`; build with `syncGroup.org()/user()/of()` from
534
+ * `<namespace>:<id>`; build with `syncGroup(kind, id)` from
564
535
  * `@abloatai/ablo/schema`). Omit for the server default:
565
536
  * `[org:<your org>, user:<user.id>]`. */
566
537
  syncGroups?: readonly SyncGroupInput[];
@@ -585,7 +556,7 @@ export interface CreateAgentSessionParams<S extends SchemaRecord> {
585
556
  [M in keyof S & string]?: readonly SessionOperation[];
586
557
  };
587
558
  /** Sync groups this session may subscribe to — typed (`'default'` or
588
- * `<namespace>:<id>`; build with `syncGroup.org()/user()/of()` from
559
+ * `<namespace>:<id>`; build with `syncGroup(kind, id)` from
589
560
  * `@abloatai/ablo/schema`). Omit for the server default: the org
590
561
  * anchor (`org:<your org>`) + the agent's own anchor. */
591
562
  syncGroups?: readonly SyncGroupInput[];
@@ -667,7 +638,7 @@ export type Ablo<S extends SchemaRecord> = {
667
638
  * Replace the bearer auth token used for the WebSocket upgrade and HTTP
668
639
  * requests, WITHOUT tearing down the engine. Use to push a refreshed
669
640
  * short-lived access key (the Stripe-style `ek_`/`rk_`) before it expires —
670
- * `<AbloProvider>`'s `getToken` refresh loop calls this. Reuses the same
641
+ * the client's `apiKey`-resolver refresh loop calls this. Reuses the same
671
642
  * rotation path as the internal capability-token refresh; safe to call before
672
643
  * `ready()`. Also nudges a parked connection to re-probe with the new token.
673
644
  */
@@ -675,8 +646,8 @@ export type Ablo<S extends SchemaRecord> = {
675
646
  /**
676
647
  * Resolve the active bearer credential this engine authenticates with — the
677
648
  * live `ek_`/`rk_` the WebSocket and HTTP transports currently carry (kept
678
- * fresh by the `getToken` refresh loop), falling back to a configured API
679
- * key. Returns `null` when no credential is set yet. Use it to authenticate
649
+ * fresh by the `apiKey`-resolver refresh loop), falling back to a configured
650
+ * API key. Returns `null` when no credential is set yet. Use it to authenticate
680
651
  * a side-band request to the same server with the very token this client
681
652
  * already holds — no extra mint round-trip.
682
653
  */
@@ -685,10 +656,10 @@ export type Ablo<S extends SchemaRecord> = {
685
656
  * Register a re-mint hook for the short-lived access key. The connection
686
657
  * layer calls it WHEN it finds the key stale (a `credential_stale` probe) or
687
658
  * on an external nudge; the hook mints a fresh `ek_`/`rk_` from the still-valid
688
- * login. Mirrors the `getToken` contract: resolve a token, resolve `null` when
689
- * the login itself is gone (→ sign out), or THROW on a transient failure (→
690
- * back off, never sign out). `<AbloProvider>` wires this from its
691
- * `getToken`/`authEndpoint`. Safe to call before `ready()`.
659
+ * login. Mirrors the `apiKey`-resolver contract: resolve a token, resolve
660
+ * `null` when the login itself is gone (→ sign out), or THROW on a transient
661
+ * failure (→ back off, never sign out). The client wires this automatically
662
+ * from a function `apiKey`. Safe to call before `ready()`.
692
663
  */
693
664
  setCredentialRefresher(refresher: (() => Promise<string | null>) | null): void;
694
665
  /**
@@ -703,14 +674,15 @@ export type Ablo<S extends SchemaRecord> = {
703
674
  * Mint a short-lived, scoped **session token** for one end user — the
704
675
  * Stripe `ephemeralKeys.create` / Supabase session shape. Call this on YOUR
705
676
  * BACKEND (where the `sk_` secret key lives), then hand the returned
706
- * `token` to that user's browser (typically via an authEndpoint the client
707
- * fetches). The browser presents it as the bearer; the sync-server verifies
677
+ * `token` to that user's browser (typically via a token route the browser's
678
+ * `apiKey` resolver fetches). The browser presents it as the bearer; the sync-server verifies
708
679
  * it via `apiKeyProvider`.
709
680
  *
710
681
  * The browser must NEVER see the `sk_` key — only the per-user session token.
711
682
  *
712
683
  * Pass `{ user: { id } }` for a full-authority end-user session (mints `ek_`,
713
- * `actor_kind: 'user'` attribution), or `{ agent: { id }, can: { tasks:
684
+ * `participantKind: 'user'` attribution, stored as `actor_kind` on the delta
685
+ * row), or `{ agent: { id }, can: { tasks:
714
686
  * ['update'] } }` for a scoped agent session (mints `rk_`); `can` is typed
715
687
  * against your schema's model names. Always authenticates with the original
716
688
  * `sk_` — never the client's exchanged sync credential.
@@ -822,24 +794,6 @@ export type Ablo<S extends SchemaRecord> = {
822
794
  * are schema-powered sugar over the same model write/read path.
823
795
  */
824
796
  model<T = Record<string, unknown>>(name: string): ModelClient<T>;
825
- /**
826
- * Canonical multiplayer participant surface. Joins a structured app
827
- * target, derives the transport scope internally, opens a scoped
828
- * claim on the existing WebSocket, and returns target-bound presence
829
- * + claim helpers.
830
- *
831
- * ```ts
832
- * const participant = await ablo.participants.join({
833
- * type: 'File',
834
- * id: 'src/foo.ts',
835
- * path: 'src/foo.ts',
836
- * range: { startLine: 10, endLine: 40 },
837
- * });
838
- * participant.presence.editing();
839
- * const claim = participant.claims.claim('rewrite imports');
840
- * ```
841
- */
842
- readonly participants: ParticipantManager;
843
797
  /**
844
798
  * Capture a context-staleness watermark over a set of entities.
845
799
  * Returns a flat snapshot with `stamp` (thread into writes as
@@ -991,9 +945,6 @@ export declare namespace Ablo {
991
945
  type ClaimLost = _Streams.ClaimLost;
992
946
  type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
993
947
  namespace Auth {
994
- type Principal = _Streams.Principal;
995
- type Session = _Streams.SessionRef;
996
- type Agent = _Streams.AgentRef;
997
948
  type Actor = _Streams.ParticipantRef;
998
949
  }
999
950
  namespace Participant {