@homespunapps/core 1.0.0 → 1.4.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.
package/dist/client.d.ts CHANGED
@@ -90,6 +90,20 @@ export declare class HomespunClient {
90
90
  * self-destruct. Returns 204 with no body on success.
91
91
  */
92
92
  revokeKey(id: string): Promise<void>;
93
+ /**
94
+ * POST /v1/keys: mint a NEW sibling API key for the calling agent's OWN
95
+ * identity. The relay derives the identity from the bearer token, so a caller
96
+ * can only ever mint a sibling of itself (never another agent's key). The new
97
+ * key has the same scope/ownership as the caller and shows up in a subsequent
98
+ * `listKeys()` call made WITH the new key.
99
+ *
100
+ * The raw `api_key` is returned exactly ONCE in this response and is never
101
+ * retrievable again (only its hash is stored). Bootstraps a fresh CLI /
102
+ * process credential from an MCP-driven agent that has no key of its own to
103
+ * hand off. The owner can revoke any minted sibling via the normal revoke
104
+ * path.
105
+ */
106
+ mintKey(): Promise<KeyMintResult>;
93
107
  /**
94
108
  * POST /v1/agents/claim — bind this agent to a human via a one-shot
95
109
  * claim code the human generated in their settings UI. After a
@@ -159,6 +173,25 @@ export declare class HomespunClient {
159
173
  * stream bytes through the relay.
160
174
  */
161
175
  uploadBlob(file: Blob | Buffer | Uint8Array, opts?: UploadBlobOptions): Promise<AttachmentRef>;
176
+ /**
177
+ * Upload a attachment from INLINE base64 bytes: the no-filesystem sibling of
178
+ * `uploadBlob()`. Use this when the bytes were produced in memory (an image
179
+ * the agent generated, a document it assembled) and there is no local file to
180
+ * stream: an MCP client running inside a Claude session has no filesystem to
181
+ * point `uploadBlob()` at.
182
+ *
183
+ * The bytes are sent as base64 in a JSON body to the relay's
184
+ * `POST /v1/attachments` inline variant ({ content_base64, filename?, mime?,
185
+ * scope?, app_id? }). The relay decodes them behind a pre-decode size guard and runs
186
+ * the IDENTICAL validation pipeline the multipart `uploadBlob()` path runs:
187
+ * magic-byte MIME sniff, BLOB_MIME_ALLOWLIST, per-attachment size cap, and the
188
+ * per-agent / per-app / per-account quota reservation. `mime` is advisory
189
+ * (the relay sniffs the real type regardless), and an oversized or disallowed
190
+ * inline upload returns the same errors the file path would.
191
+ *
192
+ * @param contentBase64 standard base64 (e.g. `Buffer.from(bytes).toString("base64")`).
193
+ */
194
+ uploadBlobInline(contentBase64: string, opts?: UploadBlobOptions): Promise<AttachmentRef>;
162
195
  /** GET /v1/attachments/:id — download bytes as an ArrayBuffer. */
163
196
  downloadBlob(attachmentId: string): Promise<ArrayBuffer>;
164
197
  /**
@@ -219,12 +252,36 @@ export declare class HomespunClient {
219
252
  upload_url: string;
220
253
  expires_at: string;
221
254
  }>;
222
- /** Finalise a presigned upload — relay HEADs the bytes, verifies, flips ready. */
255
+ /**
256
+ * Finalise a presigned upload. After the client PUTs the bytes to the
257
+ * `upload_url` from `presignBlob()`, the relay re-reads the stored bytes and
258
+ * runs the SAME validation a normal upload runs: it BYTE-SNIFFS the actual
259
+ * content and derives the stored/served mime from that (never the
260
+ * presign-declared mime), re-verifies size + sha256, enforces the allowlist +
261
+ * quota, and runs the scan hook, before flipping the attachment to `ready`.
262
+ * A finalize whose bytes fail any check leaves the attachment unready (it is
263
+ * never served) and returns a clean error. Returns the ready `AttachmentRef`.
264
+ */
223
265
  confirmBlob(attachmentId: string): Promise<AttachmentRef>;
266
+ /**
267
+ * Alias for {@link confirmBlob}: the "finalize" half of the presign ->
268
+ * PUT -> finalize flow, named to match the MCP `attachments` action.
269
+ */
270
+ finalizeBlob(attachmentId: string): Promise<AttachmentRef>;
224
271
  /** POST /v1/apps — create (deploy) a new App + its first AppVersion. */
225
272
  deployApp(req: DeployAppRequest): Promise<DeployAppResponse>;
226
273
  /** POST /v1/apps/:id/versions — redeploy (compat-gated unless force). */
227
274
  redeployApp(appId: string, req: RedeployAppRequest): Promise<RedeployAppResponse>;
275
+ /**
276
+ * Dry-run a deploy: validate the manifest + asset shapes, run the compat gate
277
+ * (for a redeploy) and compute the deploy advisories, WITHOUT creating a
278
+ * version or mutating anything. Posts the deploy body with `dry_run: true` to
279
+ * the same route the real deploy uses (`POST /v1/apps` for a create check,
280
+ * `POST /v1/apps/:id/versions` for a redeploy check), so the relay returns the
281
+ * SAME validation error a real deploy would for an invalid manifest, and the
282
+ * SAME warnings/compat result for a valid one. Nothing is persisted.
283
+ */
284
+ checkDeploy(req: DeployCheckRequest): Promise<DeployCheckResult>;
228
285
  /**
229
286
  * GET /v1/apps — list apps scoped to the calling agent's owning human.
230
287
  * `slug` is an exact-match filter — the one case a caller resolves a
@@ -238,10 +295,35 @@ export declare class HomespunClient {
238
295
  }): Promise<AppsPage>;
239
296
  /** GET /v1/apps/:id — full app detail (manifest, current_version, quota). */
240
297
  getApp(appId: string): Promise<AppDetail>;
241
- /** PATCH /v1/apps/:id — update visibility (the only mutable app field). */
242
- updateApp(appId: string, visibility: "private" | "link" | "public"): Promise<{
298
+ /**
299
+ * PATCH /v1/apps/:id updates the app's mutable settings. Both fields are
300
+ * optional but at least one must be given: `visibility` (private|link|public)
301
+ * and/or `timezone` (an IANA zone name like "Europe/Berlin", used for
302
+ * `schedules` reminders). Returns the updated `{ id, visibility, timezone }`.
303
+ * A transition INTO `link` visibility also returns a `share_url` carrying the
304
+ * app's freshly minted share token in its `#k=` fragment, shown ONCE (the
305
+ * token is hashed at rest and never recoverable). A transition AWAY from
306
+ * `link` clears the token (every prior share URL stops working).
307
+ */
308
+ updateApp(appId: string, update: {
309
+ visibility?: "private" | "link" | "public";
310
+ timezone?: string;
311
+ }): Promise<{
243
312
  id: string;
244
313
  visibility: string;
314
+ timezone: string | null;
315
+ share_url?: string;
316
+ }>;
317
+ /**
318
+ * POST /v1/apps/:id/share-link/rotate rotates a `link` app's share token.
319
+ * Mints a fresh token and returns the new `share_url` (its `#k=` fragment
320
+ * carries the raw token, shown ONCE). Rotating instantly revokes the previous
321
+ * share URL and every pass derived from it. Also serves as "generate" for a
322
+ * `link` app that has no share token yet. Only valid for a `link` app; a
323
+ * public/private app returns a conflict.
324
+ */
325
+ rotateShareLink(appId: string): Promise<{
326
+ share_url: string;
245
327
  }>;
246
328
  /** DELETE /v1/apps/:id — soft-delete (idempotent). */
247
329
  deleteApp(appId: string): Promise<void>;
@@ -250,6 +332,23 @@ export declare class HomespunClient {
250
332
  id: string;
251
333
  status: string;
252
334
  }>;
335
+ /**
336
+ * POST /v1/apps/:id/domain - bind a custom domain to the app. Returns the
337
+ * record including `dns_records`: the DNS entries the domain owner must
338
+ * publish (the routing CNAME plus any Cloudflare validation records).
339
+ */
340
+ setAppDomain(appId: string, domain: string): Promise<AppDomain>;
341
+ /**
342
+ * GET /v1/apps/:id/domain - the app's domain record, live-refreshed against
343
+ * Cloudflare when the feature is enabled (status/last_error/last_checked_at
344
+ * update as a side effect).
345
+ */
346
+ getAppDomain(appId: string): Promise<AppDomain>;
347
+ /**
348
+ * DELETE /v1/apps/:id/domain - remove the domain binding (Cloudflare
349
+ * hostname best-effort + record + quota release). Idempotent.
350
+ */
351
+ deleteAppDomain(appId: string): Promise<void>;
253
352
  /**
254
353
  * POST /v1/apps/:id/members — add or invite a member by email. If a Human
255
354
  * already exists for the email, the member row is created immediately
@@ -260,21 +359,107 @@ export declare class HomespunClient {
260
359
  addAppMember(appId: string, opts: {
261
360
  email: string;
262
361
  role?: "member";
362
+ customRole?: string;
263
363
  }): Promise<AddAppMemberResult>;
264
364
  /** GET /v1/apps/:id/members — list the app's owner + member rows. */
265
365
  listAppMembers(appId: string): Promise<{
266
366
  members: AppMember[];
267
367
  }>;
368
+ /**
369
+ * PATCH /v1/apps/:id/members/:humanId — change an existing member's custom
370
+ * role in place. `customRole: null` clears it back to a plain member. The
371
+ * role must be one the app's manifest declares (a reserved base role or an
372
+ * undeclared name is a 400), and the app owner's own membership cannot be
373
+ * re-roled.
374
+ *
375
+ * Unlike remove-then-re-add, this does NOT revoke the member's live app
376
+ * sessions: they stay signed in and pick up the new role on their next
377
+ * request. Roles are not cached: they are re-derived from the Member row on
378
+ * every request, so a downgrade takes effect immediately and a live session
379
+ * can never escalate.
380
+ */
381
+ setAppMemberRole(appId: string, humanId: string, opts: {
382
+ customRole: string | null;
383
+ }): Promise<{
384
+ member: AppMember;
385
+ }>;
386
+ /**
387
+ * GET /v1/apps/:id/roles — the derived roles summary: every custom role the
388
+ * app's manifest declares, what holding it actually allows per collection
389
+ * (reported separately for members and for grant-link holders, whose role
390
+ * floors differ), and how many members and live grant links hold it.
391
+ * Read-only and computed on the fly; permissions themselves stay
392
+ * manifest-declared. An app that declares no roles returns an empty list.
393
+ */
394
+ listAppRoles(appId: string): Promise<{
395
+ roles: AppRoleSummary[];
396
+ }>;
268
397
  /**
269
398
  * DELETE /v1/apps/:id/members/:humanId — remove a member (idempotent);
270
399
  * cascades to revoke that human's live app sessions. The app owner cannot
271
400
  * be removed — the relay refuses with a 409 conflict.
272
401
  */
273
402
  removeAppMember(appId: string, humanId: string): Promise<void>;
274
- /** GET /v1/apps/:id/collections/:name — list rows (current-state page). */
403
+ /**
404
+ * GET /v1/apps/:id/ingest-hooks lists the app's inbound catch-hooks with the
405
+ * full secret URL (decrypted server-side), the current rule metadata
406
+ * (collection/mode/wake/handshake), and per-hook delivery counts by status.
407
+ * The URL is the app owner's to share with a sender; it is never exposed on a
408
+ * public path, only here to the app's own owner/agent.
409
+ */
410
+ listIngestHooks(appId: string): Promise<{
411
+ hooks: IngestHookInfo[];
412
+ }>;
413
+ /**
414
+ * POST /v1/apps/:id/ingest-hooks/:name/rotate mints a fresh secret for one
415
+ * hook and return its new full URL once. No redeploy needed; the old URL stops
416
+ * working immediately.
417
+ */
418
+ rotateIngestHook(appId: string, name: string): Promise<{
419
+ hook: {
420
+ name: string;
421
+ url: string;
422
+ };
423
+ }>;
424
+ /**
425
+ * POST /v1/apps/:id/grants: mint a grant link. `role` must be a declared
426
+ * custom role for the app. `mode` is "once" (one-time, first-browser-claims)
427
+ * or "multi" (shared, capped by `maxUses` within expiry); defaults to "multi".
428
+ * An optional `pin` NARROWS the holder to a single `rowKey` OR a `where`
429
+ * filter (never widens). Returns the once-only `grant_url` carrying the token.
430
+ */
431
+ mintAppGrant(appId: string, opts: {
432
+ role: string;
433
+ mode?: "once" | "multi";
434
+ maxUses?: number;
435
+ label?: string;
436
+ ttlSeconds?: number;
437
+ pin?: {
438
+ rowKey?: string;
439
+ where?: ListWhereCondition[];
440
+ };
441
+ }): Promise<MintAppGrantResult>;
442
+ /** GET /v1/apps/:id/grants: list the app's grant links (never any token). */
443
+ listAppGrants(appId: string): Promise<{
444
+ grants: AppGrantSummary[];
445
+ }>;
446
+ /** DELETE /v1/apps/:id/grants/:grantId: revoke one grant link (idempotent). */
447
+ revokeAppGrant(appId: string, grantId: string): Promise<void>;
448
+ /**
449
+ * GET /v1/apps/:id/collections/:name — list rows (current-state page).
450
+ *
451
+ * Wave C2 structured read query: pass `where` (an AND of `{field, op, value}`
452
+ * conditions) and/or `sort` (`{field, dir}` list) to filter/order DB-side. The
453
+ * relay applies read permission + author scoping FIRST, then the filter, so a
454
+ * filtered read is always a subset of what the caller could already read. The
455
+ * query is serialized into the `q` param as URL-encoded JSON. Note: a custom
456
+ * `sort` cannot be combined with cursor pagination (`since`) in this version.
457
+ */
275
458
  listAppRows(appId: string, collection: string, opts?: {
276
459
  since?: string;
277
460
  limit?: number;
461
+ where?: ListWhereCondition[];
462
+ sort?: ListSortSpec[];
278
463
  }): Promise<AppRowsPage>;
279
464
  /**
280
465
  * GET /v1/apps/:id/collections/:name/:key — read one row. A dedicated
@@ -293,10 +478,28 @@ export declare class HomespunClient {
293
478
  upsertAppRow(appId: string, collection: string, body: {
294
479
  key?: string;
295
480
  data: unknown;
481
+ on?: string;
296
482
  }): Promise<{
297
483
  row: AppRow;
298
484
  deduped?: true;
299
485
  }>;
486
+ /**
487
+ * POST /v1/apps/:id/collections/:name with `{ on, data }`, natural-key upsert
488
+ * (Wave C1). Matches the existing row whose `<field>` value equals data[field]
489
+ * and updates it in place (idempotent re-import), else creates a fresh row.
490
+ * `field` must be declared in the collection's manifest `unique` list.
491
+ */
492
+ upsertAppRowOn(appId: string, collection: string, field: string, data: unknown): Promise<{
493
+ row: AppRow;
494
+ deduped?: true;
495
+ }>;
496
+ /**
497
+ * DELETE /v1/apps/:id/collections/:name/:key/purge, owner/agent-only removal
498
+ * that bypasses an append-only collection (Wave C1). Frees the row's unique
499
+ * values and writes an audited delete feed entry. A missing/already-deleted key
500
+ * is a 404 (HomespunApiError `row_not_found`), matching a normal delete.
501
+ */
502
+ purgeAppRow(appId: string, collection: string, key: string): Promise<void>;
300
503
  /** PATCH /v1/apps/:id/collections/:name/:key — optimistic-locked update. */
301
504
  updateAppRow(appId: string, collection: string, key: string, body: {
302
505
  data: unknown;
@@ -308,6 +511,20 @@ export declare class HomespunClient {
308
511
  deleteAppRow(appId: string, collection: string, key: string, opts?: {
309
512
  ifMatch?: number;
310
513
  }): Promise<void>;
514
+ /**
515
+ * POST /v1/apps/:id/collections/:name/batch: bulk create/upsert (Wave B).
516
+ * Returns a per-row result array: a single invalid row is reported by index
517
+ * without aborting the good rows. A batch over the server's BATCH_MAX_ROWS cap
518
+ * is a clean 400 (throws HomespunApiError `invalid_request`). DEFAULTS TO
519
+ * SILENT (suppresses notify + webhooks): pass `{emitEffects:true}` or
520
+ * `{suppress:[]}` to fire effects. Owner/agent key only for suppression control.
521
+ */
522
+ batchRows(appId: string, collection: string, rows: BatchRowInput[], opts?: BatchWriteOptions): Promise<BatchResult>;
523
+ /**
524
+ * DELETE /v1/apps/:id/collections/:name/batch: bulk soft-delete (Wave B).
525
+ * Same per-row result shape + cap as `batchRows`.
526
+ */
527
+ deleteRows(appId: string, collection: string, keys: string[]): Promise<BatchResult>;
311
528
  /**
312
529
  * GET /v1/apps/:id/feed — change-feed catch-up; the long-poll fallback
313
530
  * transport for `watch` (spec-cli §2.3/§5). `wait` (0-30s) long-polls when
@@ -318,6 +535,61 @@ export declare class HomespunClient {
318
535
  limit?: number;
319
536
  wait?: number;
320
537
  }): Promise<AppFeedPage>;
538
+ /** POST /v1/community/publish: publish an owned app as a pending template. */
539
+ publishCommunityTemplate(req: PublishCommunityTemplateRequest): Promise<PublishCommunityTemplateResponse>;
540
+ /** GET /v1/community/pending (operator): list pending submissions. */
541
+ listCommunitySubmissions(opts?: {
542
+ limit?: number;
543
+ cursor?: string;
544
+ }): Promise<CommunitySubmissionsPage>;
545
+ /** GET /v1/community/submissions/:id (operator): a submission's full content. */
546
+ getCommunitySubmission(snapshotId: string): Promise<CommunitySubmissionDetail>;
547
+ /**
548
+ * POST /v1/community/submissions/:id/{approve,reject} (operator): review a
549
+ * submission. `decision` "reject" requires a `note` (delivered to the
550
+ * publisher's app feed).
551
+ */
552
+ reviewCommunitySubmission(snapshotId: string, review: {
553
+ decision: "approve" | "reject";
554
+ note?: string;
555
+ }): Promise<CommunitySubmissionDetail>;
556
+ /** GET /v1/publisher: the caller's own publisher profile. */
557
+ getPublisher(): Promise<PublisherProfile>;
558
+ /** POST /v1/publisher/claim: set the handle once (permanent after claiming). */
559
+ claimPublisherHandle(handle: string): Promise<PublisherProfile>;
560
+ /** POST /v1/publisher/update: update displayName/bio/url. */
561
+ updatePublisher(update: PublisherProfileUpdate): Promise<PublisherProfile>;
562
+ /**
563
+ * POST /v1/community/publishers/:handle/trust (operator, marketplace PR 11):
564
+ * set a publisher's trust level ("new" | "established"), the MVP promotion
565
+ * path for the review fast-track. Operator-gated server-side; returns the
566
+ * updated publisher profile.
567
+ */
568
+ setPublisherTrustLevel(handle: string, trustLevel: "new" | "established"): Promise<PublisherProfile>;
569
+ /**
570
+ * POST /v1/reviews: review a template the caller installed. Identify the
571
+ * template by its namespaced `<handle>/<slug>` OR by explicit handle + slug.
572
+ */
573
+ createReview(req: CreateCommunityReviewRequest): Promise<CommunityReview>;
574
+ /** POST /v1/reviews/:id/respond: respond to a review as the publisher. */
575
+ respondToReview(reviewId: string, response: string | null): Promise<CommunityReview>;
576
+ /** POST /v1/reviews/:id/report: flag a review for operator attention. */
577
+ reportReview(reviewId: string, reason: string): Promise<CommunityReviewReportResult>;
578
+ /** POST /v1/reviews/:id/remove (operator): take a review down. */
579
+ removeReview(reviewId: string): Promise<CommunityReview>;
580
+ /** POST /v1/reviews/:id/unhold (operator): publish a held review. */
581
+ unholdReview(reviewId: string): Promise<CommunityReview>;
582
+ }
583
+ /**
584
+ * One multi-file-deploy asset: bytes the deployed page references by a stable,
585
+ * app-relative, same-origin path (e.g. `frames/000.jpg`). `content_base64` is the
586
+ * standard base64 of the raw bytes; `mime` is advisory (the relay sniffs the
587
+ * real type from the leading bytes).
588
+ */
589
+ export interface AppAsset {
590
+ path: string;
591
+ content_base64: string;
592
+ mime?: string;
321
593
  }
322
594
  /** Request body for `POST /v1/apps` — create (deploy) a new App. */
323
595
  export interface DeployAppRequest {
@@ -327,6 +599,13 @@ export interface DeployAppRequest {
327
599
  visibility?: "private" | "link" | "public";
328
600
  /** Accepted for visibility public|private (including the omitted default); rejected for link. */
329
601
  slug?: string;
602
+ /**
603
+ * Optional asset bundle shipped alongside the HTML in ONE deploy. Each asset
604
+ * is validated + stored app-scoped exactly like an attachment and served at
605
+ * its `path` on the app's own origin, so `<img src="frames/000.jpg">` just
606
+ * works. Rejected atomically if any asset fails validation.
607
+ */
608
+ assets?: AppAsset[];
330
609
  }
331
610
  export interface DeployAppResponse {
332
611
  app_id: string;
@@ -335,6 +614,20 @@ export interface DeployAppResponse {
335
614
  url: string;
336
615
  version: number;
337
616
  created: true;
617
+ /**
618
+ * The tokenized share URL, present ONLY when the app was created as `link`. Its
619
+ * `#k=` fragment carries the raw share token and is shown exactly ONCE (the
620
+ * token is hashed at rest and never recoverable). Anyone with this exact URL
621
+ * can open the app until it is rotated (`rotateShareLink`).
622
+ */
623
+ share_url?: string;
624
+ /**
625
+ * Non-fatal deploy advisories, present only when there is at least one. Today
626
+ * the schedules-without-timezone case: an app that declares `schedules` but has
627
+ * no timezone set will fire reminders at 08:00 UTC until one is set. Relay these
628
+ * to the human.
629
+ */
630
+ warnings?: string[];
338
631
  }
339
632
  /** Request body for `POST /v1/apps/:id/versions` — redeploy (compat-gated). */
340
633
  export interface RedeployAppRequest {
@@ -342,6 +635,11 @@ export interface RedeployAppRequest {
342
635
  manifest: unknown;
343
636
  /** Bypass the compat gate; a narrowing collection is detached, not deleted. */
344
637
  force?: boolean;
638
+ /**
639
+ * Optional asset bundle for this version. Replaces the previous version's asset
640
+ * set atomically (the new version carries its own map; old assets are detached).
641
+ */
642
+ assets?: AppAsset[];
345
643
  }
346
644
  export interface RedeployAppResponse {
347
645
  app_id: string;
@@ -351,6 +649,66 @@ export interface RedeployAppResponse {
351
649
  path: string;
352
650
  message: string;
353
651
  }>;
652
+ /**
653
+ * Non-fatal deploy advisories, present only when there is at least one; see
654
+ * DeployAppResponse.warnings. On redeploy the schedules-without-timezone
655
+ * warning fires only when the app STILL has no timezone set.
656
+ */
657
+ warnings?: string[];
658
+ }
659
+ /**
660
+ * Result of a DRY-RUN deploy (`checkDeploy`): the deploy validation + compat
661
+ * gate run WITHOUT creating a version or mutating anything. Mirrors what a real
662
+ * deploy would report: the manifest is validated (a structurally invalid
663
+ * manifest throws the SAME error a real deploy would), asset shapes are checked,
664
+ * the schedule-timezone / migration-mode advisories are computed, and (for a
665
+ * redeploy) the compat gate runs against the current version.
666
+ *
667
+ * - `ok` is true when a REAL deploy with the same inputs would succeed: a
668
+ * clean deploy, or a narrowing redeploy with `force: true`. It is false
669
+ * when a real un-forced redeploy would be REJECTED for narrowing the
670
+ * manifest (see `compat: "incompatible"` + `breaks`).
671
+ * - `compat` is present only for a redeploy check: "clean" (no narrowing),
672
+ * "forced" (narrows, but force was set so the real deploy would detach and
673
+ * proceed), or "incompatible" (narrows and force was NOT set, so the real
674
+ * deploy would return manifest_incompatible_redeploy).
675
+ * - `breaks` lists the narrowings, present only when there is at least one.
676
+ */
677
+ export interface DeployCheckResult {
678
+ ok: boolean;
679
+ warnings: string[];
680
+ compat?: "clean" | "forced" | "incompatible";
681
+ breaks?: Array<{
682
+ path: string;
683
+ message: string;
684
+ }>;
685
+ }
686
+ /**
687
+ * Request for a dry-run deploy check. Omit `app_id` to check a CREATE; pass it
688
+ * to check a REDEPLOY of an existing app (the compat gate then runs against its
689
+ * current version, and `force` decides whether a narrowing would be accepted).
690
+ */
691
+ export interface DeployCheckRequest {
692
+ app_id?: string;
693
+ html: string;
694
+ manifest: unknown;
695
+ force?: boolean;
696
+ assets?: AppAsset[];
697
+ }
698
+ /**
699
+ * Result of minting a sibling API key for the calling agent's own identity
700
+ * (`mintKey`). The raw `api_key` is returned exactly ONCE here and is never
701
+ * retrievable again (only its hash is stored). The new key has the same
702
+ * scope/ownership as the caller and can be revoked by the owner via the normal
703
+ * revoke path.
704
+ */
705
+ export interface KeyMintResult {
706
+ agent_id: string;
707
+ /** The raw sibling key, shown ONCE, never returned again. */
708
+ api_key: string;
709
+ key_prefix: string;
710
+ name: string | null;
711
+ created_at: string;
354
712
  }
355
713
  /** Lean per-app summary — the shape `listApps` returns. */
356
714
  export interface AppSummary {
@@ -359,6 +717,12 @@ export interface AppSummary {
359
717
  visibility: string;
360
718
  status: string;
361
719
  url: string;
720
+ /**
721
+ * Whether the app currently has an active share token (a `link` app with a
722
+ * live, revocable share link). NEVER the token itself: the raw token is
723
+ * returned only at create/rotate. False for public/private apps.
724
+ */
725
+ has_share_token: boolean;
362
726
  created_at: string;
363
727
  last_activity_at: string;
364
728
  }
@@ -369,11 +733,35 @@ export interface AppDetail extends AppSummary {
369
733
  owner_human_id: string;
370
734
  row_count: number;
371
735
  storage_bytes: string;
736
+ /**
737
+ * The app's IANA timezone for `schedules` reminders, or null when unset. Set
738
+ * it with `updateApp(id, { timezone })`; an app that declares schedules with no
739
+ * timezone fires reminders at 08:00 UTC.
740
+ */
741
+ timezone: string | null;
372
742
  }
373
743
  export interface AppsPage {
374
744
  items: AppSummary[];
375
745
  next_cursor: string | null;
376
746
  }
747
+ /** One DNS record the domain owner must publish (custom domains). */
748
+ export interface AppDomainDnsRecord {
749
+ type: string;
750
+ name: string;
751
+ value: string;
752
+ purpose: "routing" | "ownership" | "certificate_validation";
753
+ }
754
+ /** The app's custom-domain record - the shape the /domain routes return. */
755
+ export interface AppDomain {
756
+ domain: string;
757
+ status: "pending" | "active" | "error" | "removed";
758
+ dns_records: AppDomainDnsRecord[];
759
+ cf_hostname_id: string | null;
760
+ last_error: string | null;
761
+ last_checked_at: string | null;
762
+ created_at: string;
763
+ updated_at: string;
764
+ }
377
765
  /**
378
766
  * One member row — the shape `listAppMembers` returns and the `member` half
379
767
  * of `addAppMember`'s union returns. Fields are camelCase on the wire (see
@@ -383,8 +771,71 @@ export interface AppMember {
383
771
  humanId: string;
384
772
  email: string;
385
773
  role: string;
774
+ /** M5: an optional DECLARED custom role attached alongside base member powers. */
775
+ customRole?: string | null;
386
776
  createdAt: string;
387
777
  }
778
+ /**
779
+ * One inbound catch-hook row from `listIngestHooks`. `url` is the full secret
780
+ * ingest URL to hand a sender. Rule-derived fields (collection/mode/wake/
781
+ * handshake) are null when the hook's rule has left the manifest (disabledAt
782
+ * set). `deliveries` are per-status counts of the hook's inbound journal.
783
+ */
784
+ export interface IngestHookInfo {
785
+ name: string;
786
+ url: string;
787
+ collection: string | null;
788
+ mode: string | null;
789
+ wake: boolean | null;
790
+ handshake: string | null;
791
+ disabledAt: string | null;
792
+ createdAt: string;
793
+ deliveries: {
794
+ accepted: number;
795
+ failed: number;
796
+ dropped_duplicate: number;
797
+ };
798
+ }
799
+ /**
800
+ * Effective per-verb access from `listAppRoles`: "all" = every row, "own" =
801
+ * only rows the holder authored (the `:own` / author narrowing), "none" =
802
+ * nothing. `create` is never "own" (no pre-existing row to be the author of).
803
+ * The relay computes these by probing its real enforcement functions, so they
804
+ * report what a holder can ACTUALLY do, including floors like `anyone`.
805
+ */
806
+ export interface AppRoleVerbAccess {
807
+ read: "all" | "own" | "none";
808
+ create: "all" | "none";
809
+ update: "all" | "own" | "none";
810
+ delete: "all" | "own" | "none";
811
+ }
812
+ /**
813
+ * One collection's access from `listAppRoles`, reported PER POPULATION:
814
+ * `member_access` is what a signed-in member holding the role can do (their
815
+ * principal also carries the member floor), `grant_access` what a grant-link
816
+ * holder of the same role can do (no member floor, so the two can differ).
817
+ * `append_only` is context: such a collection refuses update/delete for every
818
+ * role while still allowing create, and the access tables reflect that.
819
+ */
820
+ export interface AppRoleCollectionAccess {
821
+ name: string;
822
+ member_access: AppRoleVerbAccess;
823
+ grant_access: AppRoleVerbAccess;
824
+ append_only: boolean;
825
+ }
826
+ /**
827
+ * One declared custom role from `listAppRoles`. `member_count` and
828
+ * `active_grant_count` are separate numbers, never summed: members are
829
+ * signed-in Humans, grant-link holders are anonymous per-holder identities.
830
+ */
831
+ export interface AppRoleSummary {
832
+ name: string;
833
+ label: string;
834
+ description: string | null;
835
+ collections: AppRoleCollectionAccess[];
836
+ member_count: number;
837
+ active_grant_count: number;
838
+ }
388
839
  /**
389
840
  * `POST /v1/apps/:id/members` response — either the member was attached
390
841
  * immediately (existing Human) or an invite email was sent (no Human yet).
@@ -396,6 +847,32 @@ export type AddAppMemberResult = {
396
847
  invited: string;
397
848
  expires_at: string;
398
849
  };
850
+ /** `POST /v1/apps/:id/grants` response. `grant_url` carries the raw link token
851
+ * in its #g= fragment and is shown ONCE (it is never recoverable afterward). */
852
+ export interface MintAppGrantResult {
853
+ id: string;
854
+ grant_url: string;
855
+ role: string;
856
+ mode: string;
857
+ max_uses: number | null;
858
+ expires_at: string;
859
+ }
860
+ /** One grant-link row from `listAppGrants` (never carries any token material). */
861
+ export interface AppGrantSummary {
862
+ id: string;
863
+ role: string;
864
+ mode: string;
865
+ max_uses: number | null;
866
+ use_count: number;
867
+ claim_count: number;
868
+ pin_row_key: string | null;
869
+ pin_where: unknown;
870
+ label: string | null;
871
+ active: boolean;
872
+ expires_at: string;
873
+ revoked_at: string | null;
874
+ created_at: string;
875
+ }
399
876
  /** One row in an app collection — the shape every row CRUD op returns. */
400
877
  export interface AppRow {
401
878
  key: string;
@@ -414,6 +891,68 @@ export interface AppRowsPage {
414
891
  next_cursor: string | null;
415
892
  has_more: boolean;
416
893
  }
894
+ /** A scalar operand for a structured read-query condition (Wave C2). */
895
+ export type ListScalar = string | number | boolean;
896
+ /**
897
+ * One structured read-query condition (Wave C2). `op` is one of eq / neq / in /
898
+ * notIn / gt / lt / gte / lte, with the SAME type semantics as a notify `when`:
899
+ * comparisons are same-type only (a number never matches a string operand), and
900
+ * dates are compared as ISO-8601 strings. `value` is a single scalar for the
901
+ * scalar ops and a non-empty scalar array for `in`/`notIn`.
902
+ */
903
+ export interface ListWhereCondition {
904
+ field: string;
905
+ op: "eq" | "neq" | "in" | "notIn" | "gt" | "lt" | "gte" | "lte";
906
+ value: ListScalar | ListScalar[];
907
+ }
908
+ /** One structured read-query sort key (Wave C2). `dir` defaults to "asc". */
909
+ export interface ListSortSpec {
910
+ field: string;
911
+ dir?: "asc" | "desc";
912
+ }
913
+ /** One row of a batch write (Wave B). `key` absent => create; present => upsert. */
914
+ export interface BatchRowInput {
915
+ key?: string;
916
+ data: unknown;
917
+ }
918
+ /**
919
+ * The outcome of one row in a batch, addressed by its `index` in the input array
920
+ * so a caller maps a failure back to the exact row it sent. `ok:true` carries the
921
+ * written `key`; `ok:false` carries the per-row `error`.
922
+ */
923
+ export interface BatchRowResult {
924
+ index: number;
925
+ ok: boolean;
926
+ key?: string;
927
+ error?: {
928
+ code: string;
929
+ message: string;
930
+ status: number;
931
+ details?: unknown;
932
+ };
933
+ }
934
+ /** The batch write / delete envelope: per-row results plus convenience counts. */
935
+ export interface BatchResult {
936
+ results: BatchRowResult[];
937
+ ok_count: number;
938
+ error_count: number;
939
+ }
940
+ /**
941
+ * Effect-suppression options for a batch (silent migration, Wave B). The batch
942
+ * endpoint DEFAULTS to silent (suppresses both notify and webhooks). Pass
943
+ * `emitEffects:true` (or `suppress:[]`) to fire effects, or `suppress` to mute a
944
+ * specific subset. Honored only for an owner/agent key.
945
+ */
946
+ export interface BatchWriteOptions {
947
+ suppress?: ("notify" | "webhooks")[];
948
+ emitEffects?: boolean;
949
+ /**
950
+ * Wave C1 natural-key upsert: match (or create) each row on this
951
+ * declared-unique field's value instead of the row id, making a re-import
952
+ * idempotent. Mutually exclusive with per-row `key`.
953
+ */
954
+ on?: string;
955
+ }
417
956
  /**
418
957
  * One entry in an app's change feed — the SAME shape whether it arrives via
419
958
  * `GET /v1/apps/:id/feed` (long-poll) or the `/_hs/ws` live/batch frames
@@ -438,6 +977,239 @@ export interface AppFeedPage {
438
977
  cursor: number;
439
978
  truncated: boolean;
440
979
  }
980
+ /** Request body for `publishCommunityTemplate()`. */
981
+ /**
982
+ * One typed agent-setup step (marketplace PR 9). An ordered list of these tells
983
+ * an installing agent what to configure after install. Carries only the
984
+ * publisher's own defaults + hints, never an installer's value; `secret: true`
985
+ * marks a step whose eventual value is sensitive (its default is masked on the
986
+ * public detail page). The relay validates + normalizes on publish.
987
+ */
988
+ export interface CommunitySetupStep {
989
+ kind: "config" | "seed-data" | "connect" | "note" | "upload";
990
+ label: string;
991
+ description?: string;
992
+ required?: boolean;
993
+ secret?: boolean;
994
+ default?: string;
995
+ choices?: string[];
996
+ valueHint?: string;
997
+ /**
998
+ * The settings-collection field this step's answer is written into
999
+ * (install-config programme). Required for an `upload` step, optional for a
1000
+ * `config` step, and not allowed on the other kinds. Field-key identifier
1001
+ * (letters, digits, '_', up to 64 chars).
1002
+ */
1003
+ key?: string;
1004
+ }
1005
+ export interface PublishCommunityTemplateRequest {
1006
+ /** The owned app to publish. */
1007
+ appId: string;
1008
+ /** Listing title; defaults server-side to the app's manifest name. */
1009
+ title?: string;
1010
+ /** Listing blurb; defaults server-side to the manifest description. */
1011
+ description?: string;
1012
+ /**
1013
+ * Optional long-form description (template-experience PR 5a): richer prose
1014
+ * rendered on the detail page below the short blurb. Plain text, length-capped
1015
+ * server-side; blank lines become escaped paragraph breaks (never raw HTML).
1016
+ */
1017
+ longDescription?: string;
1018
+ /** Optional category (validated server-side against the fixed enum). */
1019
+ category?: string;
1020
+ /** Optional curation tags. */
1021
+ tags?: string[];
1022
+ /**
1023
+ * Optional per-publisher slug (marketplace PR 2). When set, the template gets
1024
+ * a namespaced id `<handle>/<slug>` and joins a versioned line.
1025
+ */
1026
+ slug?: string;
1027
+ /** Optional semver version; defaults server-side to "1.0.0". */
1028
+ version?: string;
1029
+ /** Optional note recorded in this version's changelog entry. */
1030
+ changelogNote?: string;
1031
+ /**
1032
+ * Optional ordered typed setup steps (marketplace PR 9). Validated + stored
1033
+ * server-side; read back through the detail page + get_submission.
1034
+ */
1035
+ setupSteps?: CommunitySetupStep[];
1036
+ /** Optional remix/fork lineage: the snapshot id this was derived from. */
1037
+ derivedFromSnapshotId?: string;
1038
+ /**
1039
+ * The example-only attestation (marketplace PR 10). Set true to attest the
1040
+ * captured template content AND its captured seed rows contain no real
1041
+ * personal data: publishing makes both PUBLIC to all platform users. Recorded
1042
+ * (not a hard gate) and surfaced to the operator review payload; omitting it
1043
+ * still publishes but is shown to the reviewer as "not attested".
1044
+ */
1045
+ attestExampleOnly?: boolean;
1046
+ }
1047
+ export interface PublishCommunityTemplateResponse {
1048
+ snapshot_id: string;
1049
+ review_status: "pending" | "approved" | "rejected";
1050
+ name: string;
1051
+ seeded_row_count: number;
1052
+ slug: string | null;
1053
+ version: string;
1054
+ /** How many typed setup steps were stored (marketplace PR 9). */
1055
+ setup_step_count: number;
1056
+ /**
1057
+ * The example-only attestation as recorded (marketplace PR 10): true/false as
1058
+ * attested, or null when the publish carried no attestation.
1059
+ */
1060
+ attest_example_only: boolean | null;
1061
+ /**
1062
+ * Fast-track outcome (marketplace PR 11). `expedited`: the publisher is
1063
+ * `established` and the submission still landed `pending`, flagged to sort
1064
+ * first in the operator review queue. `auto_approved`: the publisher is
1065
+ * `established` AND the relay enables true auto-approve, so it was approved
1066
+ * immediately at publish (review_status === "approved"). Both false for a
1067
+ * `new` publisher (ordinary full review).
1068
+ */
1069
+ expedited: boolean;
1070
+ auto_approved: boolean;
1071
+ }
1072
+ /** A pending-submission summary in the operator review queue. */
1073
+ export interface CommunitySubmissionSummary {
1074
+ snapshot_id: string;
1075
+ name: string;
1076
+ description: string | null;
1077
+ /**
1078
+ * Publisher-provided long-form description (template-experience PR 5a),
1079
+ * surfaced in the review payload so the operator sees the full prose that
1080
+ * renders on the detail page. Null when the publish carried none.
1081
+ */
1082
+ long_description: string | null;
1083
+ category: string | null;
1084
+ tags: string[];
1085
+ review_status: "pending" | "approved" | "rejected";
1086
+ publisher_human_id: string | null;
1087
+ publisher_name: string | null;
1088
+ source_app_id: string | null;
1089
+ seeded_row_count: number;
1090
+ published_at: string;
1091
+ /** Namespaced identity + version (marketplace PR 2); null when unset. */
1092
+ slug: string | null;
1093
+ version: string | null;
1094
+ /**
1095
+ * Publish-time PII attestation (marketplace PR 10): true/false as attested by
1096
+ * the publisher, or null when the publish carried no attestation. Surfaced in
1097
+ * the review queue so a missing attestation is visible to the operator.
1098
+ */
1099
+ attest_example_only: boolean | null;
1100
+ /**
1101
+ * Fast-track flag (marketplace PR 11): true when this pending submission was
1102
+ * expedited (published by an `established` publisher, sorted first in the
1103
+ * queue). False for a `new` publisher and pre-PR-11 rows.
1104
+ */
1105
+ expedited: boolean;
1106
+ /** The publisher's trust level (marketplace PR 11): "new" | "established". */
1107
+ publisher_trust_level: string;
1108
+ }
1109
+ export interface CommunitySubmissionsPage {
1110
+ items: CommunitySubmissionSummary[];
1111
+ next_cursor: string | null;
1112
+ }
1113
+ /** One collection's captured-seed footprint (marketplace PR 10). */
1114
+ export interface CommunitySeedCollectionSummary {
1115
+ collection: string;
1116
+ row_count: number;
1117
+ bytes: number;
1118
+ }
1119
+ /** A per-collection digest of a submission's captured seed rows (PR 10). */
1120
+ export interface CommunitySeedRowSummary {
1121
+ collections: CommunitySeedCollectionSummary[];
1122
+ total_rows: number;
1123
+ total_bytes: number;
1124
+ }
1125
+ /** A submission's FULL content, for operator review. */
1126
+ export interface CommunitySubmissionDetail extends CommunitySubmissionSummary {
1127
+ html: string;
1128
+ manifest: unknown;
1129
+ seed_rows: unknown;
1130
+ /**
1131
+ * A digest of seed_rows (marketplace PR 10): per-collection row counts + byte
1132
+ * sizes plus totals, so the operator sees how much (and which) captured
1133
+ * starter data would become public without parsing the raw rows.
1134
+ */
1135
+ seed_summary: CommunitySeedRowSummary;
1136
+ version_id: string | null;
1137
+ reviewed_at: string | null;
1138
+ review_note: string | null;
1139
+ superseded_at: string | null;
1140
+ /** Namespaced identity + fork/license fields (marketplace PR 2). */
1141
+ changelog: unknown;
1142
+ license: string | null;
1143
+ license_terms: string | null;
1144
+ derived_from_snapshot_id: string | null;
1145
+ /**
1146
+ * Typed agent-setup steps (marketplace PR 9): the machine-readable structure
1147
+ * an agent reads. Full structure incl. secret-flagged steps (a step stores
1148
+ * only the publisher's default/hint, never an installer value). `null` when
1149
+ * the template declares no steps.
1150
+ */
1151
+ setup_steps: CommunitySetupStep[] | null;
1152
+ }
1153
+ /** A publisher's own profile, as returned by the `/v1/publisher` routes. */
1154
+ export interface PublisherProfile {
1155
+ handle: string;
1156
+ display_name: string | null;
1157
+ bio: string | null;
1158
+ url: string | null;
1159
+ /** True once the handle has been claimed (and is therefore permanent). */
1160
+ claimed: boolean;
1161
+ claimed_at: string | null;
1162
+ created_at: string;
1163
+ first_published_at: string | null;
1164
+ approved_template_count: number;
1165
+ rating_count: number;
1166
+ rating_sum: number;
1167
+ trust_level: string;
1168
+ }
1169
+ /**
1170
+ * The mutable profile fields for `updatePublisher()`. A field left `undefined`
1171
+ * is untouched; `null` clears it.
1172
+ */
1173
+ export interface PublisherProfileUpdate {
1174
+ displayName?: string | null;
1175
+ bio?: string | null;
1176
+ url?: string | null;
1177
+ }
1178
+ /** Request body for `createReview()`. Give `template` OR (`handle` + `slug`). */
1179
+ export interface CreateCommunityReviewRequest {
1180
+ /** Namespaced template id, "<handle>/<slug>". */
1181
+ template?: string;
1182
+ /** Publisher handle (with `slug`, as an alternative to `template`). */
1183
+ handle?: string;
1184
+ /** Per-publisher slug (with `handle`). */
1185
+ slug?: string;
1186
+ /** Star rating, integer 1..5. */
1187
+ stars: number;
1188
+ /** Optional written review body. */
1189
+ body?: string;
1190
+ }
1191
+ /** A review as returned by the `/v1/reviews` routes. */
1192
+ export interface CommunityReview {
1193
+ id: string;
1194
+ template_publisher_human_id: string;
1195
+ template_slug: string | null;
1196
+ snapshot_id: string;
1197
+ install_id: string | null;
1198
+ reviewer_human_id: string;
1199
+ stars: number;
1200
+ body: string | null;
1201
+ /** "visible" | "held" | "removed". A body with a link/email lands "held". */
1202
+ status: string;
1203
+ held_reason: string | null;
1204
+ publisher_response: string | null;
1205
+ publisher_responded_at: string | null;
1206
+ created_at: string;
1207
+ }
1208
+ /** Result of `reportReview()`. */
1209
+ export interface CommunityReviewReportResult {
1210
+ report_id: string;
1211
+ review_id: string;
1212
+ }
441
1213
  /** Per-attachment metadata as returned by `POST /v1/attachments` and friends. */
442
1214
  export interface AttachmentRef {
443
1215
  attachment_id: string;