@homespunapps/core 1.0.1 → 1.4.3

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
@@ -258,6 +272,16 @@ export declare class HomespunClient {
258
272
  deployApp(req: DeployAppRequest): Promise<DeployAppResponse>;
259
273
  /** POST /v1/apps/:id/versions — redeploy (compat-gated unless force). */
260
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>;
261
285
  /**
262
286
  * GET /v1/apps — list apps scoped to the calling agent's owning human.
263
287
  * `slug` is an exact-match filter — the one case a caller resolves a
@@ -271,10 +295,35 @@ export declare class HomespunClient {
271
295
  }): Promise<AppsPage>;
272
296
  /** GET /v1/apps/:id — full app detail (manifest, current_version, quota). */
273
297
  getApp(appId: string): Promise<AppDetail>;
274
- /** PATCH /v1/apps/:id — update visibility (the only mutable app field). */
275
- 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<{
276
312
  id: string;
277
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;
278
327
  }>;
279
328
  /** DELETE /v1/apps/:id — soft-delete (idempotent). */
280
329
  deleteApp(appId: string): Promise<void>;
@@ -310,21 +359,107 @@ export declare class HomespunClient {
310
359
  addAppMember(appId: string, opts: {
311
360
  email: string;
312
361
  role?: "member";
362
+ customRole?: string;
313
363
  }): Promise<AddAppMemberResult>;
314
364
  /** GET /v1/apps/:id/members — list the app's owner + member rows. */
315
365
  listAppMembers(appId: string): Promise<{
316
366
  members: AppMember[];
317
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
+ }>;
318
397
  /**
319
398
  * DELETE /v1/apps/:id/members/:humanId — remove a member (idempotent);
320
399
  * cascades to revoke that human's live app sessions. The app owner cannot
321
400
  * be removed — the relay refuses with a 409 conflict.
322
401
  */
323
402
  removeAppMember(appId: string, humanId: string): Promise<void>;
324
- /** 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
+ */
325
458
  listAppRows(appId: string, collection: string, opts?: {
326
459
  since?: string;
327
460
  limit?: number;
461
+ where?: ListWhereCondition[];
462
+ sort?: ListSortSpec[];
328
463
  }): Promise<AppRowsPage>;
329
464
  /**
330
465
  * GET /v1/apps/:id/collections/:name/:key — read one row. A dedicated
@@ -343,10 +478,28 @@ export declare class HomespunClient {
343
478
  upsertAppRow(appId: string, collection: string, body: {
344
479
  key?: string;
345
480
  data: unknown;
481
+ on?: string;
346
482
  }): Promise<{
347
483
  row: AppRow;
348
484
  deduped?: true;
349
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>;
350
503
  /** PATCH /v1/apps/:id/collections/:name/:key — optimistic-locked update. */
351
504
  updateAppRow(appId: string, collection: string, key: string, body: {
352
505
  data: unknown;
@@ -358,6 +511,20 @@ export declare class HomespunClient {
358
511
  deleteAppRow(appId: string, collection: string, key: string, opts?: {
359
512
  ifMatch?: number;
360
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>;
361
528
  /**
362
529
  * GET /v1/apps/:id/feed — change-feed catch-up; the long-poll fallback
363
530
  * transport for `watch` (spec-cli §2.3/§5). `wait` (0-30s) long-polls when
@@ -386,6 +553,48 @@ export declare class HomespunClient {
386
553
  decision: "approve" | "reject";
387
554
  note?: string;
388
555
  }): Promise<CommunitySubmissionDetail>;
556
+ /**
557
+ * GET /v1/community/templates/:ref/config-contract: the install-time config
558
+ * contract for an installable template (its settings collection + the ordered
559
+ * keyed config/upload steps). Use it to discover what a template needs and to
560
+ * pre-upload files for `upload` steps (POST /v1/attachments) before install.
561
+ */
562
+ getCommunityConfigContract(ref: string): Promise<CommunityConfigContract>;
563
+ /**
564
+ * POST /v1/community/templates/:ref/install: install a community template for
565
+ * the calling agent's owning human. `config` is `{ [stepKey]: value }` where a
566
+ * `config` step's value is a string and an `upload` step's value is an
567
+ * attachment id the agent pre-uploaded (agent-scoped) via `uploadBlob()` /
568
+ * `uploadBlobInline()`; the relay re-points it to the new app on install.
569
+ * Installs always create; returns the new app's id, slug, and url.
570
+ */
571
+ installCommunityTemplate(ref: string, config?: Record<string, unknown>): Promise<CommunityInstallResult>;
572
+ /** GET /v1/publisher: the caller's own publisher profile. */
573
+ getPublisher(): Promise<PublisherProfile>;
574
+ /** POST /v1/publisher/claim: set the handle once (permanent after claiming). */
575
+ claimPublisherHandle(handle: string): Promise<PublisherProfile>;
576
+ /** POST /v1/publisher/update: update displayName/bio/url. */
577
+ updatePublisher(update: PublisherProfileUpdate): Promise<PublisherProfile>;
578
+ /**
579
+ * POST /v1/community/publishers/:handle/trust (operator, marketplace PR 11):
580
+ * set a publisher's trust level ("new" | "established"), the MVP promotion
581
+ * path for the review fast-track. Operator-gated server-side; returns the
582
+ * updated publisher profile.
583
+ */
584
+ setPublisherTrustLevel(handle: string, trustLevel: "new" | "established"): Promise<PublisherProfile>;
585
+ /**
586
+ * POST /v1/reviews: review a template the caller installed. Identify the
587
+ * template by its namespaced `<handle>/<slug>` OR by explicit handle + slug.
588
+ */
589
+ createReview(req: CreateCommunityReviewRequest): Promise<CommunityReview>;
590
+ /** POST /v1/reviews/:id/respond: respond to a review as the publisher. */
591
+ respondToReview(reviewId: string, response: string | null): Promise<CommunityReview>;
592
+ /** POST /v1/reviews/:id/report: flag a review for operator attention. */
593
+ reportReview(reviewId: string, reason: string): Promise<CommunityReviewReportResult>;
594
+ /** POST /v1/reviews/:id/remove (operator): take a review down. */
595
+ removeReview(reviewId: string): Promise<CommunityReview>;
596
+ /** POST /v1/reviews/:id/unhold (operator): publish a held review. */
597
+ unholdReview(reviewId: string): Promise<CommunityReview>;
389
598
  }
390
599
  /**
391
600
  * One multi-file-deploy asset: bytes the deployed page references by a stable,
@@ -421,6 +630,20 @@ export interface DeployAppResponse {
421
630
  url: string;
422
631
  version: number;
423
632
  created: true;
633
+ /**
634
+ * The tokenized share URL, present ONLY when the app was created as `link`. Its
635
+ * `#k=` fragment carries the raw share token and is shown exactly ONCE (the
636
+ * token is hashed at rest and never recoverable). Anyone with this exact URL
637
+ * can open the app until it is rotated (`rotateShareLink`).
638
+ */
639
+ share_url?: string;
640
+ /**
641
+ * Non-fatal deploy advisories, present only when there is at least one. Today
642
+ * the schedules-without-timezone case: an app that declares `schedules` but has
643
+ * no timezone set will fire reminders at 08:00 UTC until one is set. Relay these
644
+ * to the human.
645
+ */
646
+ warnings?: string[];
424
647
  }
425
648
  /** Request body for `POST /v1/apps/:id/versions` — redeploy (compat-gated). */
426
649
  export interface RedeployAppRequest {
@@ -442,6 +665,66 @@ export interface RedeployAppResponse {
442
665
  path: string;
443
666
  message: string;
444
667
  }>;
668
+ /**
669
+ * Non-fatal deploy advisories, present only when there is at least one; see
670
+ * DeployAppResponse.warnings. On redeploy the schedules-without-timezone
671
+ * warning fires only when the app STILL has no timezone set.
672
+ */
673
+ warnings?: string[];
674
+ }
675
+ /**
676
+ * Result of a DRY-RUN deploy (`checkDeploy`): the deploy validation + compat
677
+ * gate run WITHOUT creating a version or mutating anything. Mirrors what a real
678
+ * deploy would report: the manifest is validated (a structurally invalid
679
+ * manifest throws the SAME error a real deploy would), asset shapes are checked,
680
+ * the schedule-timezone / migration-mode advisories are computed, and (for a
681
+ * redeploy) the compat gate runs against the current version.
682
+ *
683
+ * - `ok` is true when a REAL deploy with the same inputs would succeed: a
684
+ * clean deploy, or a narrowing redeploy with `force: true`. It is false
685
+ * when a real un-forced redeploy would be REJECTED for narrowing the
686
+ * manifest (see `compat: "incompatible"` + `breaks`).
687
+ * - `compat` is present only for a redeploy check: "clean" (no narrowing),
688
+ * "forced" (narrows, but force was set so the real deploy would detach and
689
+ * proceed), or "incompatible" (narrows and force was NOT set, so the real
690
+ * deploy would return manifest_incompatible_redeploy).
691
+ * - `breaks` lists the narrowings, present only when there is at least one.
692
+ */
693
+ export interface DeployCheckResult {
694
+ ok: boolean;
695
+ warnings: string[];
696
+ compat?: "clean" | "forced" | "incompatible";
697
+ breaks?: Array<{
698
+ path: string;
699
+ message: string;
700
+ }>;
701
+ }
702
+ /**
703
+ * Request for a dry-run deploy check. Omit `app_id` to check a CREATE; pass it
704
+ * to check a REDEPLOY of an existing app (the compat gate then runs against its
705
+ * current version, and `force` decides whether a narrowing would be accepted).
706
+ */
707
+ export interface DeployCheckRequest {
708
+ app_id?: string;
709
+ html: string;
710
+ manifest: unknown;
711
+ force?: boolean;
712
+ assets?: AppAsset[];
713
+ }
714
+ /**
715
+ * Result of minting a sibling API key for the calling agent's own identity
716
+ * (`mintKey`). The raw `api_key` is returned exactly ONCE here and is never
717
+ * retrievable again (only its hash is stored). The new key has the same
718
+ * scope/ownership as the caller and can be revoked by the owner via the normal
719
+ * revoke path.
720
+ */
721
+ export interface KeyMintResult {
722
+ agent_id: string;
723
+ /** The raw sibling key, shown ONCE, never returned again. */
724
+ api_key: string;
725
+ key_prefix: string;
726
+ name: string | null;
727
+ created_at: string;
445
728
  }
446
729
  /** Lean per-app summary — the shape `listApps` returns. */
447
730
  export interface AppSummary {
@@ -450,6 +733,12 @@ export interface AppSummary {
450
733
  visibility: string;
451
734
  status: string;
452
735
  url: string;
736
+ /**
737
+ * Whether the app currently has an active share token (a `link` app with a
738
+ * live, revocable share link). NEVER the token itself: the raw token is
739
+ * returned only at create/rotate. False for public/private apps.
740
+ */
741
+ has_share_token: boolean;
453
742
  created_at: string;
454
743
  last_activity_at: string;
455
744
  }
@@ -460,6 +749,12 @@ export interface AppDetail extends AppSummary {
460
749
  owner_human_id: string;
461
750
  row_count: number;
462
751
  storage_bytes: string;
752
+ /**
753
+ * The app's IANA timezone for `schedules` reminders, or null when unset. Set
754
+ * it with `updateApp(id, { timezone })`; an app that declares schedules with no
755
+ * timezone fires reminders at 08:00 UTC.
756
+ */
757
+ timezone: string | null;
463
758
  }
464
759
  export interface AppsPage {
465
760
  items: AppSummary[];
@@ -492,7 +787,70 @@ export interface AppMember {
492
787
  humanId: string;
493
788
  email: string;
494
789
  role: string;
790
+ /** M5: an optional DECLARED custom role attached alongside base member powers. */
791
+ customRole?: string | null;
792
+ createdAt: string;
793
+ }
794
+ /**
795
+ * One inbound catch-hook row from `listIngestHooks`. `url` is the full secret
796
+ * ingest URL to hand a sender. Rule-derived fields (collection/mode/wake/
797
+ * handshake) are null when the hook's rule has left the manifest (disabledAt
798
+ * set). `deliveries` are per-status counts of the hook's inbound journal.
799
+ */
800
+ export interface IngestHookInfo {
801
+ name: string;
802
+ url: string;
803
+ collection: string | null;
804
+ mode: string | null;
805
+ wake: boolean | null;
806
+ handshake: string | null;
807
+ disabledAt: string | null;
495
808
  createdAt: string;
809
+ deliveries: {
810
+ accepted: number;
811
+ failed: number;
812
+ dropped_duplicate: number;
813
+ };
814
+ }
815
+ /**
816
+ * Effective per-verb access from `listAppRoles`: "all" = every row, "own" =
817
+ * only rows the holder authored (the `:own` / author narrowing), "none" =
818
+ * nothing. `create` is never "own" (no pre-existing row to be the author of).
819
+ * The relay computes these by probing its real enforcement functions, so they
820
+ * report what a holder can ACTUALLY do, including floors like `anyone`.
821
+ */
822
+ export interface AppRoleVerbAccess {
823
+ read: "all" | "own" | "none";
824
+ create: "all" | "none";
825
+ update: "all" | "own" | "none";
826
+ delete: "all" | "own" | "none";
827
+ }
828
+ /**
829
+ * One collection's access from `listAppRoles`, reported PER POPULATION:
830
+ * `member_access` is what a signed-in member holding the role can do (their
831
+ * principal also carries the member floor), `grant_access` what a grant-link
832
+ * holder of the same role can do (no member floor, so the two can differ).
833
+ * `append_only` is context: such a collection refuses update/delete for every
834
+ * role while still allowing create, and the access tables reflect that.
835
+ */
836
+ export interface AppRoleCollectionAccess {
837
+ name: string;
838
+ member_access: AppRoleVerbAccess;
839
+ grant_access: AppRoleVerbAccess;
840
+ append_only: boolean;
841
+ }
842
+ /**
843
+ * One declared custom role from `listAppRoles`. `member_count` and
844
+ * `active_grant_count` are separate numbers, never summed: members are
845
+ * signed-in Humans, grant-link holders are anonymous per-holder identities.
846
+ */
847
+ export interface AppRoleSummary {
848
+ name: string;
849
+ label: string;
850
+ description: string | null;
851
+ collections: AppRoleCollectionAccess[];
852
+ member_count: number;
853
+ active_grant_count: number;
496
854
  }
497
855
  /**
498
856
  * `POST /v1/apps/:id/members` response — either the member was attached
@@ -505,6 +863,32 @@ export type AddAppMemberResult = {
505
863
  invited: string;
506
864
  expires_at: string;
507
865
  };
866
+ /** `POST /v1/apps/:id/grants` response. `grant_url` carries the raw link token
867
+ * in its #g= fragment and is shown ONCE (it is never recoverable afterward). */
868
+ export interface MintAppGrantResult {
869
+ id: string;
870
+ grant_url: string;
871
+ role: string;
872
+ mode: string;
873
+ max_uses: number | null;
874
+ expires_at: string;
875
+ }
876
+ /** One grant-link row from `listAppGrants` (never carries any token material). */
877
+ export interface AppGrantSummary {
878
+ id: string;
879
+ role: string;
880
+ mode: string;
881
+ max_uses: number | null;
882
+ use_count: number;
883
+ claim_count: number;
884
+ pin_row_key: string | null;
885
+ pin_where: unknown;
886
+ label: string | null;
887
+ active: boolean;
888
+ expires_at: string;
889
+ revoked_at: string | null;
890
+ created_at: string;
891
+ }
508
892
  /** One row in an app collection — the shape every row CRUD op returns. */
509
893
  export interface AppRow {
510
894
  key: string;
@@ -523,6 +907,68 @@ export interface AppRowsPage {
523
907
  next_cursor: string | null;
524
908
  has_more: boolean;
525
909
  }
910
+ /** A scalar operand for a structured read-query condition (Wave C2). */
911
+ export type ListScalar = string | number | boolean;
912
+ /**
913
+ * One structured read-query condition (Wave C2). `op` is one of eq / neq / in /
914
+ * notIn / gt / lt / gte / lte, with the SAME type semantics as a notify `when`:
915
+ * comparisons are same-type only (a number never matches a string operand), and
916
+ * dates are compared as ISO-8601 strings. `value` is a single scalar for the
917
+ * scalar ops and a non-empty scalar array for `in`/`notIn`.
918
+ */
919
+ export interface ListWhereCondition {
920
+ field: string;
921
+ op: "eq" | "neq" | "in" | "notIn" | "gt" | "lt" | "gte" | "lte";
922
+ value: ListScalar | ListScalar[];
923
+ }
924
+ /** One structured read-query sort key (Wave C2). `dir` defaults to "asc". */
925
+ export interface ListSortSpec {
926
+ field: string;
927
+ dir?: "asc" | "desc";
928
+ }
929
+ /** One row of a batch write (Wave B). `key` absent => create; present => upsert. */
930
+ export interface BatchRowInput {
931
+ key?: string;
932
+ data: unknown;
933
+ }
934
+ /**
935
+ * The outcome of one row in a batch, addressed by its `index` in the input array
936
+ * so a caller maps a failure back to the exact row it sent. `ok:true` carries the
937
+ * written `key`; `ok:false` carries the per-row `error`.
938
+ */
939
+ export interface BatchRowResult {
940
+ index: number;
941
+ ok: boolean;
942
+ key?: string;
943
+ error?: {
944
+ code: string;
945
+ message: string;
946
+ status: number;
947
+ details?: unknown;
948
+ };
949
+ }
950
+ /** The batch write / delete envelope: per-row results plus convenience counts. */
951
+ export interface BatchResult {
952
+ results: BatchRowResult[];
953
+ ok_count: number;
954
+ error_count: number;
955
+ }
956
+ /**
957
+ * Effect-suppression options for a batch (silent migration, Wave B). The batch
958
+ * endpoint DEFAULTS to silent (suppresses both notify and webhooks). Pass
959
+ * `emitEffects:true` (or `suppress:[]`) to fire effects, or `suppress` to mute a
960
+ * specific subset. Honored only for an owner/agent key.
961
+ */
962
+ export interface BatchWriteOptions {
963
+ suppress?: ("notify" | "webhooks")[];
964
+ emitEffects?: boolean;
965
+ /**
966
+ * Wave C1 natural-key upsert: match (or create) each row on this
967
+ * declared-unique field's value instead of the row id, making a re-import
968
+ * idempotent. Mutually exclusive with per-row `key`.
969
+ */
970
+ on?: string;
971
+ }
526
972
  /**
527
973
  * One entry in an app's change feed — the SAME shape whether it arrives via
528
974
  * `GET /v1/apps/:id/feed` (long-poll) or the `/_hs/ws` live/batch frames
@@ -548,6 +994,85 @@ export interface AppFeedPage {
548
994
  truncated: boolean;
549
995
  }
550
996
  /** Request body for `publishCommunityTemplate()`. */
997
+ /**
998
+ * One typed agent-setup step (marketplace PR 9). An ordered list of these tells
999
+ * an installing agent what to configure after install. Carries only the
1000
+ * publisher's own defaults + hints, never an installer's value; `secret: true`
1001
+ * marks a step whose eventual value is sensitive (its default is masked on the
1002
+ * public detail page). The relay validates + normalizes on publish.
1003
+ */
1004
+ export interface CommunitySetupStep {
1005
+ kind: "config" | "seed-data" | "connect" | "note" | "upload";
1006
+ label: string;
1007
+ description?: string;
1008
+ required?: boolean;
1009
+ secret?: boolean;
1010
+ default?: string;
1011
+ choices?: string[];
1012
+ valueHint?: string;
1013
+ /**
1014
+ * The settings-collection field this step's answer is written into
1015
+ * (install-config programme). Required for an `upload` step, optional for a
1016
+ * `config` step, and not allowed on the other kinds. Field-key identifier
1017
+ * (letters, digits, '_', up to 64 chars).
1018
+ */
1019
+ key?: string;
1020
+ /**
1021
+ * The manifest `ingest` rule this step wires up (D7). Allowed ONLY on a
1022
+ * `connect` step, optional there. Publish rejects a name the manifest does
1023
+ * not declare. After install, the installer pastes that rule's freshly
1024
+ * provisioned hook URL into the external service.
1025
+ */
1026
+ ingestRule?: string;
1027
+ }
1028
+ /**
1029
+ * One keyed config/upload step in a template's install-time config contract
1030
+ * (install-config programme, PR 4). Field names are the wire shape (snake_case
1031
+ * `value_hint`). An `upload` step's answer is an attachment id string.
1032
+ */
1033
+ export interface CommunityConfigStep {
1034
+ key: string;
1035
+ kind: "config" | "upload";
1036
+ label: string;
1037
+ description?: string;
1038
+ required: boolean;
1039
+ secret: boolean;
1040
+ choices?: string[];
1041
+ default?: string;
1042
+ value_hint?: string;
1043
+ }
1044
+ /**
1045
+ * One `connect` step in a template's contract that wires up an INBOUND hook:
1046
+ * after install, the app's own freshly provisioned hook URL for `ingest_rule`
1047
+ * has to be pasted into the external service the step describes. Read the URLs
1048
+ * from `GET /v1/apps/:app_id/ingest-hooks` once the install returns.
1049
+ */
1050
+ export interface CommunityConnectStep {
1051
+ label: string;
1052
+ description?: string;
1053
+ ingest_rule: string;
1054
+ /** Collection the hook writes into, or null when the rule went missing. */
1055
+ collection: string | null;
1056
+ mode: "append" | "upsert" | null;
1057
+ }
1058
+ /** The install-time config contract for an installable community template. */
1059
+ export interface CommunityConfigContract {
1060
+ snapshot_id: string;
1061
+ name: string;
1062
+ slug: string | null;
1063
+ /** The manifest's declared settings collection, or null when none. */
1064
+ settings_collection: string | null;
1065
+ /** Ordered keyed config/upload steps; empty when the template needs none. */
1066
+ config_steps: CommunityConfigStep[];
1067
+ /** Ordered connect steps that name an ingest rule; empty when none. */
1068
+ connect_steps: CommunityConnectStep[];
1069
+ }
1070
+ /** Result of an agent-key community install (installs always create). */
1071
+ export interface CommunityInstallResult {
1072
+ app_id: string;
1073
+ slug: string;
1074
+ url: string;
1075
+ }
551
1076
  export interface PublishCommunityTemplateRequest {
552
1077
  /** The owned app to publish. */
553
1078
  appId: string;
@@ -555,22 +1080,77 @@ export interface PublishCommunityTemplateRequest {
555
1080
  title?: string;
556
1081
  /** Listing blurb; defaults server-side to the manifest description. */
557
1082
  description?: string;
558
- /** Optional single-word category. */
1083
+ /**
1084
+ * Optional long-form description (template-experience PR 5a): richer prose
1085
+ * rendered on the detail page below the short blurb. Plain text, length-capped
1086
+ * server-side; blank lines become escaped paragraph breaks (never raw HTML).
1087
+ */
1088
+ longDescription?: string;
1089
+ /** Optional category (validated server-side against the fixed enum). */
559
1090
  category?: string;
560
1091
  /** Optional curation tags. */
561
1092
  tags?: string[];
1093
+ /**
1094
+ * Optional per-publisher slug (marketplace PR 2). When set, the template gets
1095
+ * a namespaced id `<handle>/<slug>` and joins a versioned line.
1096
+ */
1097
+ slug?: string;
1098
+ /** Optional semver version; defaults server-side to "1.0.0". */
1099
+ version?: string;
1100
+ /** Optional note recorded in this version's changelog entry. */
1101
+ changelogNote?: string;
1102
+ /**
1103
+ * Optional ordered typed setup steps (marketplace PR 9). Validated + stored
1104
+ * server-side; read back through the detail page + get_submission.
1105
+ */
1106
+ setupSteps?: CommunitySetupStep[];
1107
+ /** Optional remix/fork lineage: the snapshot id this was derived from. */
1108
+ derivedFromSnapshotId?: string;
1109
+ /**
1110
+ * The example-only attestation (marketplace PR 10). Set true to attest the
1111
+ * captured template content AND its captured seed rows contain no real
1112
+ * personal data: publishing makes both PUBLIC to all platform users. Recorded
1113
+ * (not a hard gate) and surfaced to the operator review payload; omitting it
1114
+ * still publishes but is shown to the reviewer as "not attested".
1115
+ */
1116
+ attestExampleOnly?: boolean;
562
1117
  }
563
1118
  export interface PublishCommunityTemplateResponse {
564
1119
  snapshot_id: string;
565
1120
  review_status: "pending" | "approved" | "rejected";
566
1121
  name: string;
567
1122
  seeded_row_count: number;
1123
+ slug: string | null;
1124
+ version: string;
1125
+ /** How many typed setup steps were stored (marketplace PR 9). */
1126
+ setup_step_count: number;
1127
+ /**
1128
+ * The example-only attestation as recorded (marketplace PR 10): true/false as
1129
+ * attested, or null when the publish carried no attestation.
1130
+ */
1131
+ attest_example_only: boolean | null;
1132
+ /**
1133
+ * Fast-track outcome (marketplace PR 11). `expedited`: the publisher is
1134
+ * `established` and the submission still landed `pending`, flagged to sort
1135
+ * first in the operator review queue. `auto_approved`: the publisher is
1136
+ * `established` AND the relay enables true auto-approve, so it was approved
1137
+ * immediately at publish (review_status === "approved"). Both false for a
1138
+ * `new` publisher (ordinary full review).
1139
+ */
1140
+ expedited: boolean;
1141
+ auto_approved: boolean;
568
1142
  }
569
1143
  /** A pending-submission summary in the operator review queue. */
570
1144
  export interface CommunitySubmissionSummary {
571
1145
  snapshot_id: string;
572
1146
  name: string;
573
1147
  description: string | null;
1148
+ /**
1149
+ * Publisher-provided long-form description (template-experience PR 5a),
1150
+ * surfaced in the review payload so the operator sees the full prose that
1151
+ * renders on the detail page. Null when the publish carried none.
1152
+ */
1153
+ long_description: string | null;
574
1154
  category: string | null;
575
1155
  tags: string[];
576
1156
  review_status: "pending" | "approved" | "rejected";
@@ -579,20 +1159,147 @@ export interface CommunitySubmissionSummary {
579
1159
  source_app_id: string | null;
580
1160
  seeded_row_count: number;
581
1161
  published_at: string;
1162
+ /** Namespaced identity + version (marketplace PR 2); null when unset. */
1163
+ slug: string | null;
1164
+ version: string | null;
1165
+ /**
1166
+ * Publish-time PII attestation (marketplace PR 10): true/false as attested by
1167
+ * the publisher, or null when the publish carried no attestation. Surfaced in
1168
+ * the review queue so a missing attestation is visible to the operator.
1169
+ */
1170
+ attest_example_only: boolean | null;
1171
+ /**
1172
+ * Fast-track flag (marketplace PR 11): true when this pending submission was
1173
+ * expedited (published by an `established` publisher, sorted first in the
1174
+ * queue). False for a `new` publisher and pre-PR-11 rows.
1175
+ */
1176
+ expedited: boolean;
1177
+ /** The publisher's trust level (marketplace PR 11): "new" | "established". */
1178
+ publisher_trust_level: string;
582
1179
  }
583
1180
  export interface CommunitySubmissionsPage {
584
1181
  items: CommunitySubmissionSummary[];
585
1182
  next_cursor: string | null;
586
1183
  }
1184
+ /** One collection's captured-seed footprint (marketplace PR 10). */
1185
+ export interface CommunitySeedCollectionSummary {
1186
+ collection: string;
1187
+ row_count: number;
1188
+ bytes: number;
1189
+ }
1190
+ /** A per-collection digest of a submission's captured seed rows (PR 10). */
1191
+ export interface CommunitySeedRowSummary {
1192
+ collections: CommunitySeedCollectionSummary[];
1193
+ total_rows: number;
1194
+ total_bytes: number;
1195
+ }
587
1196
  /** A submission's FULL content, for operator review. */
588
1197
  export interface CommunitySubmissionDetail extends CommunitySubmissionSummary {
589
1198
  html: string;
590
1199
  manifest: unknown;
591
1200
  seed_rows: unknown;
1201
+ /**
1202
+ * A digest of seed_rows (marketplace PR 10): per-collection row counts + byte
1203
+ * sizes plus totals, so the operator sees how much (and which) captured
1204
+ * starter data would become public without parsing the raw rows.
1205
+ */
1206
+ seed_summary: CommunitySeedRowSummary;
592
1207
  version_id: string | null;
593
1208
  reviewed_at: string | null;
594
1209
  review_note: string | null;
595
1210
  superseded_at: string | null;
1211
+ /** Namespaced identity + fork/license fields (marketplace PR 2). */
1212
+ changelog: unknown;
1213
+ license: string | null;
1214
+ license_terms: string | null;
1215
+ derived_from_snapshot_id: string | null;
1216
+ /**
1217
+ * Typed agent-setup steps (marketplace PR 9): the machine-readable structure
1218
+ * an agent reads. Full structure incl. secret-flagged steps (a step stores
1219
+ * only the publisher's default/hint, never an installer value). `null` when
1220
+ * the template declares no steps.
1221
+ */
1222
+ setup_steps: CommunitySetupStep[] | null;
1223
+ /**
1224
+ * Every external destination the manifest declares (D6 disclosure): outbound
1225
+ * webhook targets, the app's fetch allowance and its framing allowance,
1226
+ * deduplicated and HOSTS ONLY, so a reviewer sees where the app can send
1227
+ * data without reading the raw manifest. Empty when it talks to nothing
1228
+ * outside Homespun.
1229
+ */
1230
+ external_destinations: CommunityExternalDestination[];
1231
+ }
1232
+ /** One disclosed external destination on a submission (D6). */
1233
+ export interface CommunityExternalDestination {
1234
+ /** `webhook` = the relay POSTs row data there; `fetch` / `embed` = the app's
1235
+ * own page may call or frame it. */
1236
+ kind: "webhook" | "embed" | "fetch";
1237
+ /** Destination host, or null when the manifest alone cannot name it. */
1238
+ host: string | null;
1239
+ /** Settings field an install-time webhook target is read from. */
1240
+ fromSetting?: string;
1241
+ /** Stored connection a relative webhook target resolves against. */
1242
+ viaConnection?: string;
1243
+ }
1244
+ /** A publisher's own profile, as returned by the `/v1/publisher` routes. */
1245
+ export interface PublisherProfile {
1246
+ handle: string;
1247
+ display_name: string | null;
1248
+ bio: string | null;
1249
+ url: string | null;
1250
+ /** True once the handle has been claimed (and is therefore permanent). */
1251
+ claimed: boolean;
1252
+ claimed_at: string | null;
1253
+ created_at: string;
1254
+ first_published_at: string | null;
1255
+ approved_template_count: number;
1256
+ rating_count: number;
1257
+ rating_sum: number;
1258
+ trust_level: string;
1259
+ }
1260
+ /**
1261
+ * The mutable profile fields for `updatePublisher()`. A field left `undefined`
1262
+ * is untouched; `null` clears it.
1263
+ */
1264
+ export interface PublisherProfileUpdate {
1265
+ displayName?: string | null;
1266
+ bio?: string | null;
1267
+ url?: string | null;
1268
+ }
1269
+ /** Request body for `createReview()`. Give `template` OR (`handle` + `slug`). */
1270
+ export interface CreateCommunityReviewRequest {
1271
+ /** Namespaced template id, "<handle>/<slug>". */
1272
+ template?: string;
1273
+ /** Publisher handle (with `slug`, as an alternative to `template`). */
1274
+ handle?: string;
1275
+ /** Per-publisher slug (with `handle`). */
1276
+ slug?: string;
1277
+ /** Star rating, integer 1..5. */
1278
+ stars: number;
1279
+ /** Optional written review body. */
1280
+ body?: string;
1281
+ }
1282
+ /** A review as returned by the `/v1/reviews` routes. */
1283
+ export interface CommunityReview {
1284
+ id: string;
1285
+ template_publisher_human_id: string;
1286
+ template_slug: string | null;
1287
+ snapshot_id: string;
1288
+ install_id: string | null;
1289
+ reviewer_human_id: string;
1290
+ stars: number;
1291
+ body: string | null;
1292
+ /** "visible" | "held" | "removed". A body with a link/email lands "held". */
1293
+ status: string;
1294
+ held_reason: string | null;
1295
+ publisher_response: string | null;
1296
+ publisher_responded_at: string | null;
1297
+ created_at: string;
1298
+ }
1299
+ /** Result of `reportReview()`. */
1300
+ export interface CommunityReviewReportResult {
1301
+ report_id: string;
1302
+ review_id: string;
596
1303
  }
597
1304
  /** Per-attachment metadata as returned by `POST /v1/attachments` and friends. */
598
1305
  export interface AttachmentRef {