busabase-sdk 0.19.2 → 0.21.0

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.js CHANGED
@@ -136,13 +136,15 @@ const AgentSessionVOSchema = z.object({
136
136
  /** Set when status is "failed"; surfaced verbatim to the user. */
137
137
  error: z.string().nullable().default(null)
138
138
  });
139
- /** One connected agent backend in the current space and authenticated user's scope. */
139
+ /** One connected agent backend visible in the requested workspace scope. */
140
140
  const AgentConnectionVOSchema = z.object({
141
141
  slug: z.string(),
142
142
  agentName: z.string(),
143
143
  transport: AgentTransportSchema,
144
144
  sessionCount: z.number().int().nonnegative(),
145
- latest: AgentSessionVOSchema.nullable()
145
+ latest: AgentSessionVOSchema.nullable(),
146
+ /** Controls owner-only actions such as deleting the saved OAuth grant. */
147
+ ownedByCurrentUser: z.boolean()
146
148
  });
147
149
  /**
148
150
  * One streamed event from a session.
@@ -178,17 +180,34 @@ const AgentSessionEventVOSchema = z.object({
178
180
  permissionOptionId: z.string().optional(),
179
181
  at: z.string()
180
182
  });
183
+ const AgentConnectionScopeSchema = z.enum(["mine", "space"]);
184
+ const ListAgentConnectionsInputSchema = z.object({ scope: AgentConnectionScopeSchema.default("mine") });
181
185
  const CreateAgentSessionInputSchema = z.object({
182
186
  /** Must name a catalog entry. Deliberately NOT a command line — see below. */
183
187
  slug: z.string().min(1) });
184
188
  const DisconnectAgentInputSchema = z.object({
185
189
  /** The exact connected-agent slug shown by the sessions/catalog surfaces. */
186
190
  slug: z.string().min(1) });
187
- /** Base64 image/audio the browser attached — ACP's `ImageContent`/`AudioContent` shape verbatim. */
191
+ /**
192
+ * A base64 payload the browser attached.
193
+ *
194
+ * `image`/`audio` map to ACP's `ImageContent`/`AudioContent` verbatim; `file`
195
+ * is everything else (a PDF, a spreadsheet, a Markdown note) and becomes an
196
+ * ACP embedded `resource` block at send time. `data` is base64 for every kind
197
+ * — including textual files — so the browser never has to guess an encoding;
198
+ * whether a file travels to the agent as ACP text or as an ACP blob is decided
199
+ * server-side from the payload itself.
200
+ */
188
201
  const PromptAttachmentInputSchema = z.object({
189
- kind: z.enum(["image", "audio"]),
190
- data: z.string().min(1),
191
- mimeType: z.string().min(1)
202
+ kind: z.enum([
203
+ "image",
204
+ "audio",
205
+ "file"
206
+ ]),
207
+ data: z.string().min(1).max(2e7),
208
+ mimeType: z.string().min(1),
209
+ /** The original filename, carried for `file` so the agent and the transcript can name it. */
210
+ filename: z.string().max(255).optional()
192
211
  });
193
212
  const PromptAgentSessionInputSchema = z.object({
194
213
  sessionId: z.string().min(1),
@@ -219,8 +238,8 @@ const agentsContract = {
219
238
  deletedSessionCount: z.number().int().nonnegative()
220
239
  })),
221
240
  connections: {
222
- /** Connected backends, scoped to the current space and authenticated user. */
223
- list: oc.output(AgentConnectionVOSchema.array()) },
241
+ /** Connected backends in the current user's personal or active-space scope. */
242
+ list: oc.input(ListAgentConnectionsInputSchema).output(AgentConnectionVOSchema.array()) },
224
243
  sessions: {
225
244
  list: oc.output(AgentSessionVOSchema.array()),
226
245
  create: oc.input(CreateAgentSessionInputSchema).output(AgentSessionVOSchema),
@@ -663,7 +682,8 @@ const makeFileTreeNodeType = (config) => ({
663
682
  icon: config.icon,
664
683
  capabilities: {
665
684
  hasDetail: true,
666
- creatable: true
685
+ creatable: true,
686
+ publicAccess: config.publicAccess
667
687
  },
668
688
  operations: fileTreeOperations(config.type)
669
689
  });
@@ -681,7 +701,8 @@ const airappNodeType = makeFileTreeNodeType({
681
701
  icon: "app-window",
682
702
  routeBase: "airapps",
683
703
  tag: "AirApps",
684
- entryFile: "package.json"
704
+ entryFile: "package.json",
705
+ publicAccess: "no"
685
706
  });
686
707
  //#endregion
687
708
  //#region ../../packages/busabase-contract/src/domains/base/definition.ts
@@ -692,7 +713,8 @@ const baseNodeType = {
692
713
  icon: "table",
693
714
  capabilities: {
694
715
  hasDetail: true,
695
- creatable: true
716
+ creatable: true,
717
+ publicAccess: "detail"
696
718
  },
697
719
  operations: [
698
720
  {
@@ -795,7 +817,8 @@ const docNodeType = {
795
817
  icon: "file-text",
796
818
  capabilities: {
797
819
  hasDetail: true,
798
- creatable: true
820
+ creatable: true,
821
+ publicAccess: "detail"
799
822
  },
800
823
  operations: [{
801
824
  kind: "doc_update",
@@ -812,7 +835,8 @@ const driveNodeType = makeFileTreeNodeType({
812
835
  icon: "hard-drive",
813
836
  routeBase: "drives",
814
837
  tag: "Drives",
815
- entryFile: "README.md"
838
+ entryFile: "README.md",
839
+ publicAccess: "no"
816
840
  });
817
841
  //#endregion
818
842
  //#region ../../packages/busabase-contract/src/domains/file-node/definition.ts
@@ -824,7 +848,8 @@ const fileNodeType = {
824
848
  icon: "file",
825
849
  capabilities: {
826
850
  hasDetail: true,
827
- creatable: true
851
+ creatable: true,
852
+ publicAccess: "detail"
828
853
  },
829
854
  operations: []
830
855
  };
@@ -839,7 +864,8 @@ const folderNodeType = {
839
864
  capabilities: {
840
865
  container: true,
841
866
  creatable: true,
842
- hasDetail: true
867
+ hasDetail: true,
868
+ publicAccess: "detail"
843
869
  },
844
870
  operations: []
845
871
  };
@@ -860,7 +886,8 @@ const formNodeType = {
860
886
  icon: "form",
861
887
  capabilities: {
862
888
  hasDetail: true,
863
- creatable: true
889
+ creatable: true,
890
+ publicAccess: "submit"
864
891
  },
865
892
  operations: []
866
893
  };
@@ -879,7 +906,8 @@ const htmlNodeType = {
879
906
  icon: "code-xml",
880
907
  capabilities: {
881
908
  hasDetail: true,
882
- creatable: true
909
+ creatable: true,
910
+ publicAccess: "no"
883
911
  },
884
912
  operations: [{
885
913
  kind: "html_document_update",
@@ -896,7 +924,8 @@ const skillNodeType = makeFileTreeNodeType({
896
924
  icon: "sparkles",
897
925
  routeBase: "skills",
898
926
  tag: "Skills",
899
- entryFile: "SKILL.md"
927
+ entryFile: "SKILL.md",
928
+ publicAccess: "no"
900
929
  });
901
930
  //#endregion
902
931
  //#region ../../packages/busabase-contract/src/domains/whiteboard/definition.ts
@@ -912,7 +941,8 @@ const whiteboardNodeType = {
912
941
  icon: "pen-tool",
913
942
  capabilities: {
914
943
  hasDetail: true,
915
- creatable: true
944
+ creatable: true,
945
+ publicAccess: "detail"
916
946
  },
917
947
  operations: [{
918
948
  kind: "whiteboard_document_update",
@@ -935,7 +965,8 @@ const workflowNodeType = {
935
965
  icon: "workflow",
936
966
  capabilities: {
937
967
  hasDetail: true,
938
- creatable: true
968
+ creatable: true,
969
+ publicAccess: "detail"
939
970
  },
940
971
  operations: [{
941
972
  kind: "workflow_document_update",
@@ -1161,6 +1192,23 @@ const userRefSchema = z.object({
1161
1192
  image: z.string().nullable(),
1162
1193
  role: z.string().nullable().optional()
1163
1194
  });
1195
+ const sourceChannelSchema = z.enum([
1196
+ "web_ui",
1197
+ "browser",
1198
+ "openapi",
1199
+ "sdk",
1200
+ "cli",
1201
+ "mcp",
1202
+ "skill",
1203
+ "webhook",
1204
+ "automation",
1205
+ "import"
1206
+ ]);
1207
+ const sourceAttributionSchema = z.object({
1208
+ displayName: z.string().nullable(),
1209
+ ownerName: z.string().nullable(),
1210
+ channel: sourceChannelSchema.nullable()
1211
+ });
1164
1212
  const commitSchema = z.object({
1165
1213
  id: z.string(),
1166
1214
  baseId: z.string().nullable(),
@@ -1252,6 +1300,7 @@ const changeRequestSchema = z.object({
1252
1300
  status: changeRequestStatusSchema,
1253
1301
  submittedBy: z.string(),
1254
1302
  submittedByUser: userRefSchema.nullable().optional().default(null),
1303
+ sourceAttribution: sourceAttributionSchema.nullable().optional(),
1255
1304
  sourceMeta: z.record(z.string(), z.unknown()),
1256
1305
  reviewPolicySnapshot: z.record(z.string(), z.unknown()),
1257
1306
  mergeSummary: z.record(z.string(), z.unknown()),
@@ -1275,11 +1324,18 @@ const agentTaskSchema = z.object({
1275
1324
  });
1276
1325
  const searchResultSchema = z.object({
1277
1326
  id: z.string(),
1327
+ /**
1328
+ * `node` covers the CONTENT of a content-bearing node (doc / html /
1329
+ * whiteboard / workflow). Purely additive — callers that do not know it can
1330
+ * ignore the kind, and callers that never asked for it (an explicit
1331
+ * `sources` list without `nodes`) never receive it.
1332
+ */
1278
1333
  kind: z.enum([
1279
1334
  "record",
1280
1335
  "change_request",
1281
1336
  "base",
1282
- "file"
1337
+ "file",
1338
+ "node"
1283
1339
  ]),
1284
1340
  title: z.string(),
1285
1341
  body: z.string(),
@@ -1292,7 +1348,17 @@ const searchResponseSchema = z.object({
1292
1348
  limit: z.number(),
1293
1349
  offset: z.number(),
1294
1350
  hasMore: z.boolean(),
1295
- results: z.array(searchResultSchema)
1351
+ results: z.array(searchResultSchema),
1352
+ /**
1353
+ * True when at least one in-scope node's content was indexed only up to the
1354
+ * projection cap, so this search could not see all of it.
1355
+ *
1356
+ * Exists so an empty result can be reported honestly: without it, "no
1357
+ * results" is ambiguous between "the workspace does not contain this" and
1358
+ * "we did not look at all of it". Clients should surface it — and point at
1359
+ * `grep`, which has no such cap — rather than implying absence.
1360
+ */
1361
+ contentTruncated: z.boolean().default(false)
1296
1362
  });
1297
1363
  const liveEventSchema = z.object({
1298
1364
  kind: z.enum([
@@ -1340,6 +1406,7 @@ const auditEventSchema = z.object({
1340
1406
  action: auditActionSchema,
1341
1407
  actorId: z.string(),
1342
1408
  actor: userRefSchema.nullable().optional().default(null),
1409
+ sourceAttribution: sourceAttributionSchema.nullable().optional(),
1343
1410
  baseId: z.string().nullable(),
1344
1411
  recordId: z.string().nullable(),
1345
1412
  changeRequestId: z.string().nullable(),
@@ -1461,21 +1528,30 @@ const listChangeRequestsPagedInputSchema = z.object({
1461
1528
  /** Opaque base64 cursor (`createdAt|id`) for keyset pagination. */
1462
1529
  cursor: z.string().optional(),
1463
1530
  status: z.array(changeRequestStatusSchema).optional(),
1464
- mine: z.boolean().optional()
1531
+ mine: z.boolean().optional(),
1532
+ affectsNodeId: z.string().min(1).optional()
1465
1533
  }).optional().default({ limit: 50 });
1466
1534
  const listChangeRequestsResponseSchema = z.object({
1467
1535
  changeRequests: z.array(changeRequestSchema),
1468
1536
  nextCursor: z.string().nullable()
1469
1537
  });
1470
- const listChangeRequestsPageInputSchema = z.object({
1538
+ const changeRequestPageInputShape = {
1471
1539
  page: z.coerce.number().int().min(1).optional().default(1),
1472
1540
  pageSize: z.coerce.number().int().min(1).max(100).optional().default(50),
1473
1541
  status: z.array(changeRequestStatusSchema).optional(),
1474
1542
  mine: z.boolean().optional()
1543
+ };
1544
+ const listChangeRequestsPageInputSchema = z.object({
1545
+ ...changeRequestPageInputShape,
1546
+ affectsNodeId: z.string().min(1).optional()
1475
1547
  }).optional().default({
1476
1548
  page: 1,
1477
1549
  pageSize: 50
1478
1550
  });
1551
+ const inboxSnapshotInputSchema = z.object(changeRequestPageInputShape).optional().default({
1552
+ page: 1,
1553
+ pageSize: 50
1554
+ });
1479
1555
  const listChangeRequestsPageResponseSchema = z.object({
1480
1556
  changeRequests: z.array(changeRequestSchema),
1481
1557
  total: z.number().int().nonnegative(),
@@ -1495,7 +1571,8 @@ const inboxSnapshotResponseSchema = listChangeRequestsPageResponseSchema.extend(
1495
1571
  const SEARCH_SOURCES = [
1496
1572
  "records",
1497
1573
  "files",
1498
- "names"
1574
+ "names",
1575
+ "nodes"
1499
1576
  ];
1500
1577
  const searchInputSchema = z.object({
1501
1578
  query: z.string().default(""),
@@ -2763,6 +2840,19 @@ const ExportAssetTextVOSchema = z.object({
2763
2840
  textContentHash: z.string().nullable(),
2764
2841
  byteCount: z.number().int().nonnegative()
2765
2842
  });
2843
+ const ImportBeginInputSchema = z.object({
2844
+ /**
2845
+ * The space id the archive was ORIGINALLY exported from (`manifest.spaceId`
2846
+ * in the `.bbdump`, already integrity-verified before this is called).
2847
+ * `importTableRows`'s "nodes" handling needs this to recognize the
2848
+ * archive's own root-node row deterministically (`rootNodeIdForSpace`) —
2849
+ * scanning each batch's rows for "the one with a null `parentId`" only
2850
+ * works when that row happens to land in the SAME batch as its children,
2851
+ * which cursor pagination (id-ordered, not tree-ordered) does not
2852
+ * guarantee once a space has more nodes than one page. See the matching
2853
+ * comment in `import-logic.ts`.
2854
+ */
2855
+ sourceSpaceId: z.string() });
2766
2856
  const ImportBeginVOSchema = z.object({ sessionId: z.string() });
2767
2857
  /**
2768
2858
  * `docBodies`, `attachmentBlobs` and `assetTextBlobs` are pseudo-tables: doc
@@ -2826,7 +2916,7 @@ const dumpContract = {
2826
2916
  tags: ["Dump"],
2827
2917
  summary: "Begin a full-fidelity import session",
2828
2918
  successDescription: "Created an import session for the current space. Refused unless the space's node tree is empty (full-fidelity import preserves original ids and cannot merge into existing data)."
2829
- }).output(ImportBeginVOSchema),
2919
+ }).input(ImportBeginInputSchema).output(ImportBeginVOSchema),
2830
2920
  importTables: oc.route({
2831
2921
  method: "POST",
2832
2922
  path: "/dump/import/tables",
@@ -3671,6 +3761,104 @@ const listRecordActivityInputSchema = z.object({
3671
3761
  recordId: z.string().min(1),
3672
3762
  limit: z.coerce.number().int().min(1).max(100).optional().default(50)
3673
3763
  });
3764
+ const EMBED_LINK_MAX_MINUTES = 1440;
3765
+ const EmbedNodeTypeSchema = z.enum([
3766
+ "base",
3767
+ "doc",
3768
+ "file",
3769
+ "drive",
3770
+ "skill",
3771
+ "folder",
3772
+ "airapp"
3773
+ ]);
3774
+ const EmbedTargetTypeSchema = z.enum([
3775
+ "node",
3776
+ "change-request",
3777
+ "record-detail"
3778
+ ]);
3779
+ const EmbedFrameModeSchema = z.enum([
3780
+ "anywhere",
3781
+ "origins",
3782
+ "top-level-only"
3783
+ ]);
3784
+ const EmbedAllowedOriginSchema = z.string().trim().min(1).superRefine((value, ctx) => {
3785
+ let url;
3786
+ try {
3787
+ url = new URL(value);
3788
+ } catch {
3789
+ ctx.addIssue({
3790
+ code: "custom",
3791
+ message: "Allowed origins must be valid URLs"
3792
+ });
3793
+ return;
3794
+ }
3795
+ const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]");
3796
+ if (url.protocol !== "https:" && !isLocalHttp) ctx.addIssue({
3797
+ code: "custom",
3798
+ message: "Allowed origins must use HTTPS"
3799
+ });
3800
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash || url.hostname.includes("*")) ctx.addIssue({
3801
+ code: "custom",
3802
+ message: "Allowed origins must be exact origins"
3803
+ });
3804
+ }).transform((value) => new URL(value).origin);
3805
+ const EmbedFramePolicyInputSchema = z.discriminatedUnion("mode", [
3806
+ z.object({
3807
+ mode: z.literal("anywhere"),
3808
+ allowedOrigins: z.array(EmbedAllowedOriginSchema).max(0).optional()
3809
+ }),
3810
+ z.object({
3811
+ mode: z.literal("origins"),
3812
+ allowedOrigins: z.array(EmbedAllowedOriginSchema).min(1).max(20)
3813
+ }),
3814
+ z.object({
3815
+ mode: z.literal("top-level-only"),
3816
+ allowedOrigins: z.array(EmbedAllowedOriginSchema).max(0).optional()
3817
+ })
3818
+ ]).transform((policy) => ({
3819
+ mode: policy.mode,
3820
+ allowedOrigins: [...new Set(policy.allowedOrigins ?? [])]
3821
+ }));
3822
+ const EmbedFramePolicyVOSchema = z.object({
3823
+ mode: EmbedFrameModeSchema,
3824
+ allowedOrigins: z.array(z.string().url())
3825
+ }).superRefine((policy, ctx) => {
3826
+ if (!(policy.mode === "origins" ? policy.allowedOrigins.length > 0 : policy.allowedOrigins.length === 0)) ctx.addIssue({
3827
+ code: "custom",
3828
+ message: "Stored frame policy is invalid"
3829
+ });
3830
+ });
3831
+ const CreateEmbedLinkInputSchema = z.object({
3832
+ type: EmbedTargetTypeSchema,
3833
+ typeId: z.string().min(1),
3834
+ expiresInMinutes: z.number().int().min(1).max(EMBED_LINK_MAX_MINUTES).optional().default(15),
3835
+ framePolicy: EmbedFramePolicyInputSchema.optional().default({
3836
+ mode: "anywhere",
3837
+ allowedOrigins: []
3838
+ })
3839
+ });
3840
+ const ListEmbedLinksInputSchema = z.object({
3841
+ type: EmbedTargetTypeSchema.optional(),
3842
+ typeId: z.string().min(1).optional()
3843
+ }).optional().default({});
3844
+ const RevokeEmbedLinkInputSchema = z.object({ id: z.string().min(1) });
3845
+ const EmbedLinkVOSchema = z.object({
3846
+ id: z.string(),
3847
+ type: EmbedTargetTypeSchema,
3848
+ typeId: z.string(),
3849
+ targetName: z.string(),
3850
+ nodeType: EmbedNodeTypeSchema.nullable(),
3851
+ createdAt: z.string().datetime(),
3852
+ expiresAt: z.string().datetime(),
3853
+ revokedAt: z.string().datetime().nullable(),
3854
+ active: z.boolean(),
3855
+ framePolicy: EmbedFramePolicyVOSchema
3856
+ });
3857
+ const CreatedEmbedLinkVOSchema = EmbedLinkVOSchema.extend({
3858
+ url: z.string().url(),
3859
+ iframeUrl: z.string().url()
3860
+ });
3861
+ const RevokeEmbedLinkVOSchema = z.object({ revoked: z.literal(true) });
3674
3862
  //#endregion
3675
3863
  //#region ../../packages/busabase-contract/src/contract/grep-schemas.ts
3676
3864
  /**
@@ -4107,6 +4295,14 @@ const NodeDetailVOSchema = z.discriminatedUnion("type", [
4107
4295
  * hint. An ambiguous slug with no `type` is refused rather than silently
4108
4296
  * resolved to whichever row sorts first.
4109
4297
  */
4298
+ /**
4299
+ * `nodes.ancestors` output — the node's ancestor ids, ROOT-FIRST, excluding
4300
+ * the node itself. Ids only, deliberately: the sidebar already has (or can
4301
+ * lazily fetch) each ancestor's own row, and it only needs to know WHICH
4302
+ * folders to expand. Returning full nodes here would duplicate `nodes.list`
4303
+ * and make a cheap navigational lookup expensive.
4304
+ */
4305
+ const nodeAncestorsVOSchema = z.object({ ancestorIds: z.array(z.string()) });
4110
4306
  const getNodeInputSchema = z.object({
4111
4307
  nodeId: z.string().describe("Node id, or a slug that is unique within its type."),
4112
4308
  type: z.enum(NODE_TYPES).optional().describe("Optional disambiguation hint, only needed when `nodeId` is a slug that exists under more than one node type.")
@@ -4134,6 +4330,29 @@ const changeRequestBatchFailureSchema = z.object({
4134
4330
  code: z.string().optional(),
4135
4331
  data: z.unknown().optional()
4136
4332
  });
4333
+ const embedLinkErrorResponseSchema = z.object({ error: z.string() });
4334
+ const embedLinkErrors = {
4335
+ BAD_REQUEST: {
4336
+ status: 400,
4337
+ message: "Bad Request",
4338
+ data: embedLinkErrorResponseSchema
4339
+ },
4340
+ UNAUTHORIZED: {
4341
+ status: 401,
4342
+ message: "Unauthorized",
4343
+ data: embedLinkErrorResponseSchema
4344
+ },
4345
+ FORBIDDEN: {
4346
+ status: 403,
4347
+ message: "Forbidden",
4348
+ data: embedLinkErrorResponseSchema
4349
+ },
4350
+ NOT_FOUND: {
4351
+ status: 404,
4352
+ message: "Not Found",
4353
+ data: embedLinkErrorResponseSchema
4354
+ }
4355
+ };
4137
4356
  const changeRequestReviewBatchResultSchema = z.object({ results: z.array(z.discriminatedUnion("ok", [z.object({
4138
4357
  changeRequestId: z.string(),
4139
4358
  ok: z.literal(true),
@@ -4170,6 +4389,29 @@ const busabaseContractRoutes = {
4170
4389
  summary: "Search files, Docs, and Base records with one pattern (unified grep)",
4171
4390
  successDescription: "Streaming regex/literal matches across every in-scope source — Drive/Skill files, Doc bodies, and Base records (canonical headCommit.payload, never the truncated search projection) — with one shared pattern, one shared maxMatches/deadline budget (files scanned first, then docs, then whatever budget remains goes to records), and a per-source honest coverage report (files keeps its existing missing/stale/unsearchable/errored/notReached; docs and records report scanned/errored/notReached). truncated is set when any source truncated or has notReached > 0."
4172
4391
  }).input(UnifiedGrepInputSchema).output(UnifiedGrepResultVOSchema),
4392
+ embedLinks: {
4393
+ create: oc.route({
4394
+ method: "POST",
4395
+ path: "/embed-links",
4396
+ tags: ["Embed Links"],
4397
+ summary: "Create a polymorphic read-only embed link",
4398
+ successDescription: "The capability URL is returned once; only its secret hash is stored."
4399
+ }).errors(embedLinkErrors).input(CreateEmbedLinkInputSchema).output(CreatedEmbedLinkVOSchema),
4400
+ list: oc.route({
4401
+ method: "GET",
4402
+ path: "/embed-links",
4403
+ tags: ["Embed Links"],
4404
+ summary: "List embed links the caller can manage",
4405
+ successDescription: "Embed link metadata without capability secrets."
4406
+ }).errors(embedLinkErrors).input(ListEmbedLinksInputSchema).output(z.array(EmbedLinkVOSchema)),
4407
+ revoke: oc.route({
4408
+ method: "DELETE",
4409
+ path: "/embed-links/{id}",
4410
+ tags: ["Embed Links"],
4411
+ summary: "Revoke an embed link",
4412
+ successDescription: "The capability stops resolving immediately."
4413
+ }).errors(embedLinkErrors).input(RevokeEmbedLinkInputSchema).output(RevokeEmbedLinkVOSchema)
4414
+ },
4173
4415
  nodes: {
4174
4416
  list: oc.route({
4175
4417
  method: "GET",
@@ -4183,7 +4425,7 @@ const busabaseContractRoutes = {
4183
4425
  path: "/nodes/search",
4184
4426
  tags: ["Nodes", "Search"],
4185
4427
  summary: "Search nodes by name/slug (cheap, name-only quick-jump)",
4186
- successDescription: "Plain ilike match on name/slug across every registered node type, scoped by the same node-visibility ACL as `nodes.list`. No content scan and no full-text ranking — ordered exact-slug-match first, then by name. Backs the dashboard search dialog's 'Recent' tab cache-miss path; the heavier `search` endpoint remains the dedicated full-text content search."
4428
+ successDescription: "Plain ilike match on name/slug across every registered node type, scoped by the same node-visibility ACL as `nodes.list`. No content scan and no full-text ranking — ordered exact-slug-match first, then by name. Backs the dashboard search dialog's 'Recent' tab cache-miss path. To search what is written INSIDE nodes, use `search` with the `nodes` source (indexed, paginated) or `grep` (exhaustive, no index)."
4187
4429
  }).input(searchNodesByNameInputSchema).output(z.array(nodeSearchResultSchema)),
4188
4430
  isDescendant: oc.route({
4189
4431
  method: "GET",
@@ -4192,6 +4434,13 @@ const busabaseContractRoutes = {
4192
4434
  summary: "Check whether a node is a descendant of another",
4193
4435
  successDescription: "Server-authoritative parentId-chain walk from nodeId up to potentialAncestorId. Used to gate cross-branch drag-and-drop drops in the sidebar, since the full tree is no longer guaranteed to be loaded client-side (depth-bounded lazy load) — a purely local walk could wrongly allow dropping a folder into its own unloaded descendant."
4194
4436
  }).input(isDescendantInputSchema).output(isDescendantOutputSchema),
4437
+ ancestors: oc.route({
4438
+ method: "GET",
4439
+ path: "/nodes/{nodeId}/ancestors",
4440
+ tags: ["Nodes"],
4441
+ summary: "List a node's ancestor ids",
4442
+ successDescription: "The node's ancestor ids, root-first, excluding the node itself (`[]` directly under the workspace root). `nodeId` accepts an id or a slug, same as `nodes.get`; pass `type` when a slug exists under more than one type. Lets a depth-bounded, lazily-expanded tree open straight to a deep node on a cold load (a refresh, a bookmark, a shared link) without one round trip per level."
4443
+ }).input(getNodeInputSchema).output(nodeAncestorsVOSchema),
4195
4444
  createChangeRequest: oc.route({
4196
4445
  method: "POST",
4197
4446
  path: "/nodes/change-requests",
@@ -4435,16 +4684,16 @@ const busabaseContractRoutes = {
4435
4684
  path: "/change-requests",
4436
4685
  tags: ["Change Requests"],
4437
4686
  summary: "List change requests",
4438
- successDescription: "A page of change requests plus an opaque nextCursor (null at the end). Filter with `status` and/or `mine`."
4687
+ successDescription: "A page of change requests plus an opaque nextCursor (null at the end). Filter with `status`, `mine`, and/or `affectsNodeId`."
4439
4688
  }).input(listChangeRequestsPagedInputSchema).output(listChangeRequestsResponseSchema),
4440
4689
  listPage: oc.route({
4441
4690
  method: "GET",
4442
4691
  path: "/change-requests/page",
4443
4692
  tags: ["Change Requests"],
4444
4693
  summary: "List a numbered change request page",
4445
- successDescription: "A random-access page of change requests plus the total across the whole filter. Same `status` / `mine` filters as the cursor listing."
4694
+ successDescription: "A random-access page of change requests plus the total across the whole filter. Same `status`, `mine`, and `affectsNodeId` filters as the cursor listing."
4446
4695
  }).input(listChangeRequestsPageInputSchema).output(listChangeRequestsPageResponseSchema),
4447
- inboxSnapshot: oc.input(listChangeRequestsPageInputSchema).output(inboxSnapshotResponseSchema),
4696
+ inboxSnapshot: oc.input(inboxSnapshotInputSchema).output(inboxSnapshotResponseSchema),
4448
4697
  counts: oc.route({
4449
4698
  method: "GET",
4450
4699
  path: "/change-requests/counts",
@@ -4495,94 +4744,6 @@ const busabaseContractRoutes = {
4495
4744
  views: viewContract
4496
4745
  };
4497
4746
  oc.prefix("/api/v1").router(busabaseContractRoutes);
4498
- const EMBED_LINK_MAX_MINUTES = 1440;
4499
- const EmbedNodeTypeSchema = z.enum([
4500
- "base",
4501
- "doc",
4502
- "file",
4503
- "drive",
4504
- "skill",
4505
- "folder",
4506
- "airapp"
4507
- ]);
4508
- const EmbedFrameModeSchema = z.enum([
4509
- "anywhere",
4510
- "origins",
4511
- "top-level-only"
4512
- ]);
4513
- const EmbedAllowedOriginSchema = z.string().trim().min(1).superRefine((value, ctx) => {
4514
- let url;
4515
- try {
4516
- url = new URL(value);
4517
- } catch {
4518
- ctx.addIssue({
4519
- code: "custom",
4520
- message: "Allowed origins must be valid URLs"
4521
- });
4522
- return;
4523
- }
4524
- const isLocalHttp = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]");
4525
- if (url.protocol !== "https:" && !isLocalHttp) ctx.addIssue({
4526
- code: "custom",
4527
- message: "Allowed origins must use HTTPS"
4528
- });
4529
- if (url.username || url.password || url.pathname !== "/" || url.search || url.hash || url.hostname.includes("*")) ctx.addIssue({
4530
- code: "custom",
4531
- message: "Allowed origins must be exact origins"
4532
- });
4533
- }).transform((value) => new URL(value).origin);
4534
- const EmbedFramePolicyInputSchema = z.discriminatedUnion("mode", [
4535
- z.object({
4536
- mode: z.literal("anywhere"),
4537
- allowedOrigins: z.array(EmbedAllowedOriginSchema).max(0).optional()
4538
- }),
4539
- z.object({
4540
- mode: z.literal("origins"),
4541
- allowedOrigins: z.array(EmbedAllowedOriginSchema).min(1).max(20)
4542
- }),
4543
- z.object({
4544
- mode: z.literal("top-level-only"),
4545
- allowedOrigins: z.array(EmbedAllowedOriginSchema).max(0).optional()
4546
- })
4547
- ]).transform((policy) => ({
4548
- mode: policy.mode,
4549
- allowedOrigins: [...new Set(policy.allowedOrigins ?? [])]
4550
- }));
4551
- const EmbedFramePolicyVOSchema = z.object({
4552
- mode: EmbedFrameModeSchema,
4553
- allowedOrigins: z.array(z.string().url())
4554
- }).superRefine((policy, ctx) => {
4555
- if (!(policy.mode === "origins" ? policy.allowedOrigins.length > 0 : policy.allowedOrigins.length === 0)) ctx.addIssue({
4556
- code: "custom",
4557
- message: "Stored frame policy is invalid"
4558
- });
4559
- });
4560
- const CreateEmbedLinkInputSchema = z.object({
4561
- nodeId: z.string().min(1),
4562
- expiresInMinutes: z.number().int().min(1).max(EMBED_LINK_MAX_MINUTES).optional().default(15),
4563
- framePolicy: EmbedFramePolicyInputSchema.optional().default({
4564
- mode: "anywhere",
4565
- allowedOrigins: []
4566
- })
4567
- });
4568
- const ListEmbedLinksInputSchema = z.object({ nodeId: z.string().min(1).optional() }).optional().default({});
4569
- const RevokeEmbedLinkInputSchema = z.object({ id: z.string().min(1) });
4570
- const EmbedLinkVOSchema = z.object({
4571
- id: z.string(),
4572
- nodeId: z.string(),
4573
- nodeName: z.string(),
4574
- nodeType: EmbedNodeTypeSchema,
4575
- createdAt: z.string().datetime(),
4576
- expiresAt: z.string().datetime(),
4577
- revokedAt: z.string().datetime().nullable(),
4578
- active: z.boolean(),
4579
- framePolicy: EmbedFramePolicyVOSchema
4580
- });
4581
- const CreatedEmbedLinkVOSchema = EmbedLinkVOSchema.extend({
4582
- url: z.string().url(),
4583
- iframeUrl: z.string().url()
4584
- });
4585
- const RevokeEmbedLinkVOSchema = z.object({ revoked: z.literal(true) });
4586
4747
  //#endregion
4587
4748
  //#region ../../packages/busabase-contract/src/contract/cloud.ts
4588
4749
  /**
@@ -4638,25 +4799,7 @@ const notFoundErrors = { NOT_FOUND: {
4638
4799
  message: "Not Found",
4639
4800
  data: ErrorResponseSchema
4640
4801
  } };
4641
- const embedLinksErrors = {
4642
- BAD_REQUEST: {
4643
- status: 400,
4644
- message: "Bad Request",
4645
- data: ErrorResponseSchema
4646
- },
4647
- ...authenticatedErrors,
4648
- FORBIDDEN: {
4649
- status: 403,
4650
- message: "Forbidden",
4651
- data: ErrorResponseSchema
4652
- },
4653
- ...notFoundErrors
4654
- };
4655
- const securedRoute = (operation) => ({
4656
- ...operation,
4657
- security: [{ bearerAuth: [] }]
4658
- });
4659
- const { vault: _localVault, ...cloudWorkbenchRoutes } = busabaseContractRoutes;
4802
+ const { embedLinks: _embedLinksAlreadyPublishedAtRoot, vault: _localVault, ...cloudWorkbenchRoutes } = busabaseContractRoutes;
4660
4803
  const cloudExtraRoutes = {
4661
4804
  system: {
4662
4805
  health: oc.route({
@@ -4721,32 +4864,7 @@ const cloudExtraRoutes = {
4721
4864
  ...notFoundErrors
4722
4865
  }).input(z.object({ params: z.object({ id: z.string() }) })).output(AgentTaskDetailSchema)
4723
4866
  },
4724
- embedLinks: {
4725
- create: oc.route({
4726
- method: "POST",
4727
- path: "/embed-links",
4728
- tags: ["Embed Links"],
4729
- summary: "Create a short-lived read-only embed link for one node",
4730
- successDescription: "The capability URL is returned once; only its secret hash is stored.",
4731
- spec: securedRoute
4732
- }).errors(embedLinksErrors).input(CreateEmbedLinkInputSchema).output(CreatedEmbedLinkVOSchema),
4733
- list: oc.route({
4734
- method: "GET",
4735
- path: "/embed-links",
4736
- tags: ["Embed Links"],
4737
- summary: "List embed links the caller can manage",
4738
- successDescription: "Embed link metadata without capability secrets.",
4739
- spec: securedRoute
4740
- }).errors(embedLinksErrors).input(ListEmbedLinksInputSchema).output(z.array(EmbedLinkVOSchema)),
4741
- revoke: oc.route({
4742
- method: "DELETE",
4743
- path: "/embed-links/{id}",
4744
- tags: ["Embed Links"],
4745
- summary: "Revoke an embed link",
4746
- successDescription: "The capability stops resolving immediately.",
4747
- spec: securedRoute
4748
- }).errors(embedLinksErrors).input(RevokeEmbedLinkInputSchema).output(RevokeEmbedLinkVOSchema)
4749
- }
4867
+ embedLinks: busabaseContractRoutes.embedLinks
4750
4868
  };
4751
4869
  const cloudContract = oc.prefix("/api/v1").router({
4752
4870
  ...cloudWorkbenchRoutes,
@@ -4775,6 +4893,7 @@ function resolveConfig(config = {}) {
4775
4893
  webUrl: normalizeBaseUrl(config.webUrl ?? env("BUSABASE_WEB_URL") ?? baseUrl),
4776
4894
  apiKey: config.apiKey ?? env("BUSABASE_API_KEY"),
4777
4895
  spaceId: config.spaceId ?? env("BUSABASE_SPACE_ID"),
4896
+ sourceChannel: config.sourceChannel ?? "sdk",
4778
4897
  headers: config.headers,
4779
4898
  fetch: config.fetch
4780
4899
  };
@@ -4845,6 +4964,7 @@ function createBusabaseClient(config = {}) {
4845
4964
  return {
4846
4965
  ...resolved.apiKey ? { authorization: `Bearer ${resolved.apiKey}` } : {},
4847
4966
  ...resolved.spaceId ? { "x-busabase-space": resolved.spaceId } : {},
4967
+ "x-busabase-channel": resolved.sourceChannel ?? "sdk",
4848
4968
  ...extra
4849
4969
  };
4850
4970
  }
@@ -5031,7 +5151,7 @@ var Busabase = class {
5031
5151
  * This is the *authenticated*, durable link: it never expires, but the reader
5032
5152
  * needs a session unless the node has public sharing enabled. For a no-login
5033
5153
  * link, mint a Cloud embed link instead —
5034
- * `bb.embedLinks.create({ nodeId })` returns `{ url, iframeUrl }`.
5154
+ * `bb.embedLinks.create({ type: "node", typeId: nodeId })` returns `{ url, iframeUrl }`.
5035
5155
  *
5036
5156
  * @example
5037
5157
  * ```ts