@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.js CHANGED
@@ -143,6 +143,25 @@ export class HomespunClient {
143
143
  if (!r.ok)
144
144
  this.fail(r);
145
145
  }
146
+ /**
147
+ * POST /v1/keys: mint a NEW sibling API key for the calling agent's OWN
148
+ * identity. The relay derives the identity from the bearer token, so a caller
149
+ * can only ever mint a sibling of itself (never another agent's key). The new
150
+ * key has the same scope/ownership as the caller and shows up in a subsequent
151
+ * `listKeys()` call made WITH the new key.
152
+ *
153
+ * The raw `api_key` is returned exactly ONCE in this response and is never
154
+ * retrievable again (only its hash is stored). Bootstraps a fresh CLI /
155
+ * process credential from an MCP-driven agent that has no key of its own to
156
+ * hand off. The owner can revoke any minted sibling via the normal revoke
157
+ * path.
158
+ */
159
+ async mintKey() {
160
+ const r = await this.call("POST", "/v1/keys");
161
+ if (!r.ok)
162
+ this.fail(r);
163
+ return this.asObject(r);
164
+ }
146
165
  /**
147
166
  * POST /v1/agents/claim — bind this agent to a human via a one-shot
148
167
  * claim code the human generated in their settings UI. After a
@@ -510,6 +529,33 @@ export class HomespunClient {
510
529
  this.fail(r);
511
530
  return this.asObject(r);
512
531
  }
532
+ /**
533
+ * Dry-run a deploy: validate the manifest + asset shapes, run the compat gate
534
+ * (for a redeploy) and compute the deploy advisories, WITHOUT creating a
535
+ * version or mutating anything. Posts the deploy body with `dry_run: true` to
536
+ * the same route the real deploy uses (`POST /v1/apps` for a create check,
537
+ * `POST /v1/apps/:id/versions` for a redeploy check), so the relay returns the
538
+ * SAME validation error a real deploy would for an invalid manifest, and the
539
+ * SAME warnings/compat result for a valid one. Nothing is persisted.
540
+ */
541
+ async checkDeploy(req) {
542
+ const path = req.app_id !== undefined
543
+ ? `/v1/apps/${encodeURIComponent(req.app_id)}/versions`
544
+ : "/v1/apps";
545
+ const body = {
546
+ html: req.html,
547
+ manifest: req.manifest,
548
+ dry_run: true,
549
+ };
550
+ if (req.app_id !== undefined && req.force !== undefined)
551
+ body["force"] = req.force;
552
+ if (req.assets !== undefined)
553
+ body["assets"] = req.assets;
554
+ const r = await this.call("POST", path, body);
555
+ if (!r.ok)
556
+ this.fail(r);
557
+ return this.asObject(r);
558
+ }
513
559
  /**
514
560
  * GET /v1/apps — list apps scoped to the calling agent's owning human.
515
561
  * `slug` is an exact-match filter — the one case a caller resolves a
@@ -538,9 +584,37 @@ export class HomespunClient {
538
584
  this.fail(r);
539
585
  return this.asObject(r);
540
586
  }
541
- /** PATCH /v1/apps/:id — update visibility (the only mutable app field). */
542
- async updateApp(appId, visibility) {
543
- const r = await this.call("PATCH", `/v1/apps/${encodeURIComponent(appId)}`, { visibility });
587
+ /**
588
+ * PATCH /v1/apps/:id updates the app's mutable settings. Both fields are
589
+ * optional but at least one must be given: `visibility` (private|link|public)
590
+ * and/or `timezone` (an IANA zone name like "Europe/Berlin", used for
591
+ * `schedules` reminders). Returns the updated `{ id, visibility, timezone }`.
592
+ * A transition INTO `link` visibility also returns a `share_url` carrying the
593
+ * app's freshly minted share token in its `#k=` fragment, shown ONCE (the
594
+ * token is hashed at rest and never recoverable). A transition AWAY from
595
+ * `link` clears the token (every prior share URL stops working).
596
+ */
597
+ async updateApp(appId, update) {
598
+ const body = {};
599
+ if (update.visibility !== undefined)
600
+ body["visibility"] = update.visibility;
601
+ if (update.timezone !== undefined)
602
+ body["timezone"] = update.timezone;
603
+ const r = await this.call("PATCH", `/v1/apps/${encodeURIComponent(appId)}`, body);
604
+ if (!r.ok)
605
+ this.fail(r);
606
+ return this.asObject(r);
607
+ }
608
+ /**
609
+ * POST /v1/apps/:id/share-link/rotate rotates a `link` app's share token.
610
+ * Mints a fresh token and returns the new `share_url` (its `#k=` fragment
611
+ * carries the raw token, shown ONCE). Rotating instantly revokes the previous
612
+ * share URL and every pass derived from it. Also serves as "generate" for a
613
+ * `link` app that has no share token yet. Only valid for a `link` app; a
614
+ * public/private app returns a conflict.
615
+ */
616
+ async rotateShareLink(appId) {
617
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/share-link/rotate`, {});
544
618
  if (!r.ok)
545
619
  this.fail(r);
546
620
  return this.asObject(r);
@@ -601,9 +675,13 @@ export class HomespunClient {
601
675
  // (humanId/createdAt), matching packages/relay/src/http/routes/app-members.ts
602
676
  // exactly rather than the usual snake_case convention.
603
677
  //
604
- // Template `install`/`uninstall` is intentionally NOT wrapped here — that
605
- // route is `requireHuman` (human login cookie only), not agent-key
606
- // authorizable, so it has no place on this agent-facing client.
678
+ // Community template install IS agent-key authorizable and wrapped below
679
+ // (getCommunityConfigContract + installCommunityTemplate): the calling agent
680
+ // installs a community template for its OWNING human, who becomes the app's
681
+ // installer/owner, reusing the SAME core install path as the human web form.
682
+ // Trials, keep, and the first-party app `uninstall` stay human-only web flows
683
+ // (a countdown banner and a confirm page have no agent-key equivalent), so
684
+ // those remain intentionally unwrapped here.
607
685
  // -------------------------------------------------------------------------
608
686
  /**
609
687
  * POST /v1/apps/:id/members — add or invite a member by email. If a Human
@@ -613,7 +691,7 @@ export class HomespunClient {
613
691
  * EMAIL_PROVIDER=none (no invite-email path available).
614
692
  */
615
693
  async addAppMember(appId, opts) {
616
- const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/members`, { email: opts.email, role: opts.role });
694
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/members`, { email: opts.email, role: opts.role, custom_role: opts.customRole });
617
695
  if (!r.ok)
618
696
  this.fail(r);
619
697
  return this.asObject(r);
@@ -625,6 +703,39 @@ export class HomespunClient {
625
703
  this.fail(r);
626
704
  return this.asObject(r);
627
705
  }
706
+ /**
707
+ * PATCH /v1/apps/:id/members/:humanId — change an existing member's custom
708
+ * role in place. `customRole: null` clears it back to a plain member. The
709
+ * role must be one the app's manifest declares (a reserved base role or an
710
+ * undeclared name is a 400), and the app owner's own membership cannot be
711
+ * re-roled.
712
+ *
713
+ * Unlike remove-then-re-add, this does NOT revoke the member's live app
714
+ * sessions: they stay signed in and pick up the new role on their next
715
+ * request. Roles are not cached: they are re-derived from the Member row on
716
+ * every request, so a downgrade takes effect immediately and a live session
717
+ * can never escalate.
718
+ */
719
+ async setAppMemberRole(appId, humanId, opts) {
720
+ const r = await this.call("PATCH", `/v1/apps/${encodeURIComponent(appId)}/members/${encodeURIComponent(humanId)}`, { custom_role: opts.customRole });
721
+ if (!r.ok)
722
+ this.fail(r);
723
+ return this.asObject(r);
724
+ }
725
+ /**
726
+ * GET /v1/apps/:id/roles — the derived roles summary: every custom role the
727
+ * app's manifest declares, what holding it actually allows per collection
728
+ * (reported separately for members and for grant-link holders, whose role
729
+ * floors differ), and how many members and live grant links hold it.
730
+ * Read-only and computed on the fly; permissions themselves stay
731
+ * manifest-declared. An app that declares no roles returns an empty list.
732
+ */
733
+ async listAppRoles(appId) {
734
+ const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/roles`);
735
+ if (!r.ok)
736
+ this.fail(r);
737
+ return this.asObject(r);
738
+ }
628
739
  /**
629
740
  * DELETE /v1/apps/:id/members/:humanId — remove a member (idempotent);
630
741
  * cascades to revoke that human's live app sessions. The app owner cannot
@@ -636,17 +747,106 @@ export class HomespunClient {
636
747
  this.fail(r);
637
748
  }
638
749
  // -------------------------------------------------------------------------
750
+ // Inbound catch-hooks (inbound-webhooks PR 3). Read + rotate the smallest
751
+ // surface an agent needs: list an app's declared hooks with their full secret
752
+ // URL (so the agent can tell its owner the exact URL to paste into an external
753
+ // system during app setup), and rotate a leaked secret. Hooks themselves are
754
+ // manifest-declared, so there is no create/delete here.
755
+ // -------------------------------------------------------------------------
756
+ /**
757
+ * GET /v1/apps/:id/ingest-hooks lists the app's inbound catch-hooks with the
758
+ * full secret URL (decrypted server-side), the current rule metadata
759
+ * (collection/mode/wake/handshake), and per-hook delivery counts by status.
760
+ * The URL is the app owner's to share with a sender; it is never exposed on a
761
+ * public path, only here to the app's own owner/agent.
762
+ */
763
+ async listIngestHooks(appId) {
764
+ const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/ingest-hooks`);
765
+ if (!r.ok)
766
+ this.fail(r);
767
+ return this.asObject(r);
768
+ }
769
+ /**
770
+ * POST /v1/apps/:id/ingest-hooks/:name/rotate mints a fresh secret for one
771
+ * hook and return its new full URL once. No redeploy needed; the old URL stops
772
+ * working immediately.
773
+ */
774
+ async rotateIngestHook(appId, name) {
775
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/ingest-hooks/${encodeURIComponent(name)}/rotate`);
776
+ if (!r.ok)
777
+ this.fail(r);
778
+ return this.asObject(r);
779
+ }
780
+ // -------------------------------------------------------------------------
781
+ // v2 grant links (M5). An owner/agent mints a capability URL carrying a
782
+ // DECLARED custom role (x-homespun-manifest.roles) plus an optional row/filter
783
+ // pin. The raw link token rides the returned grant_url's FRAGMENT (#g=), never
784
+ // a query param, so it is never logged; the page SDK exchanges it once for a
785
+ // per-holder credential. A grant can NEVER carry a built-in role and never
786
+ // escalates past its custom role on a stable per-holder identity.
787
+ // -------------------------------------------------------------------------
788
+ /**
789
+ * POST /v1/apps/:id/grants: mint a grant link. `role` must be a declared
790
+ * custom role for the app. `mode` is "once" (one-time, first-browser-claims)
791
+ * or "multi" (shared, capped by `maxUses` within expiry); defaults to "multi".
792
+ * An optional `pin` NARROWS the holder to a single `rowKey` OR a `where`
793
+ * filter (never widens). Returns the once-only `grant_url` carrying the token.
794
+ */
795
+ async mintAppGrant(appId, opts) {
796
+ const pin = opts.pin
797
+ ? opts.pin.rowKey !== undefined
798
+ ? { row_key: opts.pin.rowKey }
799
+ : { where: opts.pin.where }
800
+ : undefined;
801
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/grants`, {
802
+ role: opts.role,
803
+ mode: opts.mode,
804
+ max_uses: opts.maxUses,
805
+ label: opts.label,
806
+ ttl_seconds: opts.ttlSeconds,
807
+ pin,
808
+ });
809
+ if (!r.ok)
810
+ this.fail(r);
811
+ return this.asObject(r);
812
+ }
813
+ /** GET /v1/apps/:id/grants: list the app's grant links (never any token). */
814
+ async listAppGrants(appId) {
815
+ const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/grants`);
816
+ if (!r.ok)
817
+ this.fail(r);
818
+ return this.asObject(r);
819
+ }
820
+ /** DELETE /v1/apps/:id/grants/:grantId: revoke one grant link (idempotent). */
821
+ async revokeAppGrant(appId, grantId) {
822
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/grants/${encodeURIComponent(grantId)}`);
823
+ if (!r.ok)
824
+ this.fail(r);
825
+ }
826
+ // -------------------------------------------------------------------------
639
827
  // v2 collection row CRUD + feed catch-up (spec-cli §2.2/§2.3). Same wire
640
828
  // shapes as the v1 homespun-records methods above, mounted under
641
829
  // /v1/apps/:id/collections/:name instead of /v1/apps/:id/records/:name.
642
830
  // -------------------------------------------------------------------------
643
- /** GET /v1/apps/:id/collections/:name — list rows (current-state page). */
831
+ /**
832
+ * GET /v1/apps/:id/collections/:name — list rows (current-state page).
833
+ *
834
+ * Wave C2 structured read query: pass `where` (an AND of `{field, op, value}`
835
+ * conditions) and/or `sort` (`{field, dir}` list) to filter/order DB-side. The
836
+ * relay applies read permission + author scoping FIRST, then the filter, so a
837
+ * filtered read is always a subset of what the caller could already read. The
838
+ * query is serialized into the `q` param as URL-encoded JSON. Note: a custom
839
+ * `sort` cannot be combined with cursor pagination (`since`) in this version.
840
+ */
644
841
  async listAppRows(appId, collection, opts = {}) {
645
842
  const q = new URLSearchParams();
646
843
  if (opts.since !== undefined)
647
844
  q.set("since", opts.since);
648
845
  if (opts.limit !== undefined)
649
846
  q.set("limit", String(opts.limit));
847
+ const query = buildListQueryParam(opts.where, opts.sort);
848
+ if (query !== undefined)
849
+ q.set("q", query);
650
850
  const qs = q.toString();
651
851
  const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/collections/${encodeURIComponent(collection)}${qs ? "?" + qs : ""}`);
652
852
  if (!r.ok)
@@ -676,6 +876,26 @@ export class HomespunClient {
676
876
  this.fail(r);
677
877
  return this.asObject(r);
678
878
  }
879
+ /**
880
+ * POST /v1/apps/:id/collections/:name with `{ on, data }`, natural-key upsert
881
+ * (Wave C1). Matches the existing row whose `<field>` value equals data[field]
882
+ * and updates it in place (idempotent re-import), else creates a fresh row.
883
+ * `field` must be declared in the collection's manifest `unique` list.
884
+ */
885
+ async upsertAppRowOn(appId, collection, field, data) {
886
+ return this.upsertAppRow(appId, collection, { on: field, data });
887
+ }
888
+ /**
889
+ * DELETE /v1/apps/:id/collections/:name/:key/purge, owner/agent-only removal
890
+ * that bypasses an append-only collection (Wave C1). Frees the row's unique
891
+ * values and writes an audited delete feed entry. A missing/already-deleted key
892
+ * is a 404 (HomespunApiError `row_not_found`), matching a normal delete.
893
+ */
894
+ async purgeAppRow(appId, collection, key) {
895
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(key)}/purge`);
896
+ if (!r.ok)
897
+ this.fail(r);
898
+ }
679
899
  /** PATCH /v1/apps/:id/collections/:name/:key — optimistic-locked update. */
680
900
  async updateAppRow(appId, collection, key, body) {
681
901
  const r = await this.call("PATCH", `/v1/apps/${encodeURIComponent(appId)}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(key)}`, body);
@@ -690,6 +910,37 @@ export class HomespunClient {
690
910
  if (!r.ok)
691
911
  this.fail(r);
692
912
  }
913
+ /**
914
+ * POST /v1/apps/:id/collections/:name/batch: bulk create/upsert (Wave B).
915
+ * Returns a per-row result array: a single invalid row is reported by index
916
+ * without aborting the good rows. A batch over the server's BATCH_MAX_ROWS cap
917
+ * is a clean 400 (throws HomespunApiError `invalid_request`). DEFAULTS TO
918
+ * SILENT (suppresses notify + webhooks): pass `{emitEffects:true}` or
919
+ * `{suppress:[]}` to fire effects. Owner/agent key only for suppression control.
920
+ */
921
+ async batchRows(appId, collection, rows, opts = {}) {
922
+ const body = { rows };
923
+ if (opts.suppress !== undefined)
924
+ body.suppress = opts.suppress;
925
+ if (opts.emitEffects !== undefined)
926
+ body.emitEffects = opts.emitEffects;
927
+ if (opts.on !== undefined)
928
+ body.on = opts.on;
929
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/collections/${encodeURIComponent(collection)}/batch`, body);
930
+ if (!r.ok)
931
+ this.fail(r);
932
+ return this.asObject(r);
933
+ }
934
+ /**
935
+ * DELETE /v1/apps/:id/collections/:name/batch: bulk soft-delete (Wave B).
936
+ * Same per-row result shape + cap as `batchRows`.
937
+ */
938
+ async deleteRows(appId, collection, keys) {
939
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/collections/${encodeURIComponent(collection)}/batch`, { keys });
940
+ if (!r.ok)
941
+ this.fail(r);
942
+ return this.asObject(r);
943
+ }
693
944
  /**
694
945
  * GET /v1/apps/:id/feed — change-feed catch-up; the long-poll fallback
695
946
  * transport for `watch` (spec-cli §2.3/§5). `wait` (0-30s) long-polls when
@@ -718,8 +969,15 @@ export class HomespunClient {
718
969
  app_id: req.appId,
719
970
  title: req.title,
720
971
  description: req.description,
972
+ long_description: req.longDescription,
721
973
  category: req.category,
722
974
  tags: req.tags,
975
+ slug: req.slug,
976
+ version: req.version,
977
+ changelog_note: req.changelogNote,
978
+ setup_steps: req.setupSteps,
979
+ derived_from_snapshot_id: req.derivedFromSnapshotId,
980
+ attest_example_only: req.attestExampleOnly,
723
981
  });
724
982
  if (!r.ok)
725
983
  this.fail(r);
@@ -757,4 +1015,139 @@ export class HomespunClient {
757
1015
  this.fail(r);
758
1016
  return this.asObject(r);
759
1017
  }
1018
+ // -------------------------------------------------------------------------
1019
+ // Agent-key community install (install-config programme, PR 4). Both act AS
1020
+ // the calling agent's owning human, who becomes the installed app's owner. A
1021
+ // `ref` is a namespaced `<handle>/<slug>` OR a community snapshot id;
1022
+ // encodeURIComponent keeps a namespaced ref one path segment on the wire.
1023
+ // -------------------------------------------------------------------------
1024
+ /**
1025
+ * GET /v1/community/templates/:ref/config-contract: the install-time config
1026
+ * contract for an installable template (its settings collection + the ordered
1027
+ * keyed config/upload steps). Use it to discover what a template needs and to
1028
+ * pre-upload files for `upload` steps (POST /v1/attachments) before install.
1029
+ */
1030
+ async getCommunityConfigContract(ref) {
1031
+ const r = await this.call("GET", `/v1/community/templates/${encodeURIComponent(ref)}/config-contract`);
1032
+ if (!r.ok)
1033
+ this.fail(r);
1034
+ return this.asObject(r);
1035
+ }
1036
+ /**
1037
+ * POST /v1/community/templates/:ref/install: install a community template for
1038
+ * the calling agent's owning human. `config` is `{ [stepKey]: value }` where a
1039
+ * `config` step's value is a string and an `upload` step's value is an
1040
+ * attachment id the agent pre-uploaded (agent-scoped) via `uploadBlob()` /
1041
+ * `uploadBlobInline()`; the relay re-points it to the new app on install.
1042
+ * Installs always create; returns the new app's id, slug, and url.
1043
+ */
1044
+ async installCommunityTemplate(ref, config) {
1045
+ const r = await this.call("POST", `/v1/community/templates/${encodeURIComponent(ref)}/install`, config !== undefined ? { config } : {});
1046
+ if (!r.ok)
1047
+ this.fail(r);
1048
+ return this.asObject(r);
1049
+ }
1050
+ // -------------------------------------------------------------------------
1051
+ // Community publisher identity (marketplace PR 1). All three act AS the
1052
+ // calling agent's owning human. claim/update additionally need a verified
1053
+ // email server-side.
1054
+ // -------------------------------------------------------------------------
1055
+ /** GET /v1/publisher: the caller's own publisher profile. */
1056
+ async getPublisher() {
1057
+ const r = await this.call("GET", "/v1/publisher");
1058
+ if (!r.ok)
1059
+ this.fail(r);
1060
+ return this.asObject(r);
1061
+ }
1062
+ /** POST /v1/publisher/claim: set the handle once (permanent after claiming). */
1063
+ async claimPublisherHandle(handle) {
1064
+ const r = await this.call("POST", "/v1/publisher/claim", { handle });
1065
+ if (!r.ok)
1066
+ this.fail(r);
1067
+ return this.asObject(r);
1068
+ }
1069
+ /** POST /v1/publisher/update: update displayName/bio/url. */
1070
+ async updatePublisher(update) {
1071
+ const r = await this.call("POST", "/v1/publisher/update", {
1072
+ display_name: update.displayName,
1073
+ bio: update.bio,
1074
+ url: update.url,
1075
+ });
1076
+ if (!r.ok)
1077
+ this.fail(r);
1078
+ return this.asObject(r);
1079
+ }
1080
+ /**
1081
+ * POST /v1/community/publishers/:handle/trust (operator, marketplace PR 11):
1082
+ * set a publisher's trust level ("new" | "established"), the MVP promotion
1083
+ * path for the review fast-track. Operator-gated server-side; returns the
1084
+ * updated publisher profile.
1085
+ */
1086
+ async setPublisherTrustLevel(handle, trustLevel) {
1087
+ const r = await this.call("POST", `/v1/community/publishers/${encodeURIComponent(handle)}/trust`, { trust_level: trustLevel });
1088
+ if (!r.ok)
1089
+ this.fail(r);
1090
+ return this.asObject(r);
1091
+ }
1092
+ // -------------------------------------------------------------------------
1093
+ // Community reviews (marketplace PR 7). createReview / respondToReview /
1094
+ // reportReview act AS the calling agent's owning human; the moderation
1095
+ // methods (removeReview / unholdReview) are operator-gated server-side.
1096
+ // -------------------------------------------------------------------------
1097
+ /**
1098
+ * POST /v1/reviews: review a template the caller installed. Identify the
1099
+ * template by its namespaced `<handle>/<slug>` OR by explicit handle + slug.
1100
+ */
1101
+ async createReview(req) {
1102
+ const r = await this.call("POST", "/v1/reviews", {
1103
+ template: req.template,
1104
+ handle: req.handle,
1105
+ slug: req.slug,
1106
+ stars: req.stars,
1107
+ body: req.body,
1108
+ });
1109
+ if (!r.ok)
1110
+ this.fail(r);
1111
+ return this.asObject(r);
1112
+ }
1113
+ /** POST /v1/reviews/:id/respond: respond to a review as the publisher. */
1114
+ async respondToReview(reviewId, response) {
1115
+ const r = await this.call("POST", `/v1/reviews/${encodeURIComponent(reviewId)}/respond`, { response });
1116
+ if (!r.ok)
1117
+ this.fail(r);
1118
+ return this.asObject(r);
1119
+ }
1120
+ /** POST /v1/reviews/:id/report: flag a review for operator attention. */
1121
+ async reportReview(reviewId, reason) {
1122
+ const r = await this.call("POST", `/v1/reviews/${encodeURIComponent(reviewId)}/report`, { reason });
1123
+ if (!r.ok)
1124
+ this.fail(r);
1125
+ return this.asObject(r);
1126
+ }
1127
+ /** POST /v1/reviews/:id/remove (operator): take a review down. */
1128
+ async removeReview(reviewId) {
1129
+ const r = await this.call("POST", `/v1/reviews/${encodeURIComponent(reviewId)}/remove`);
1130
+ if (!r.ok)
1131
+ this.fail(r);
1132
+ return this.asObject(r);
1133
+ }
1134
+ /** POST /v1/reviews/:id/unhold (operator): publish a held review. */
1135
+ async unholdReview(reviewId) {
1136
+ const r = await this.call("POST", `/v1/reviews/${encodeURIComponent(reviewId)}/unhold`);
1137
+ if (!r.ok)
1138
+ this.fail(r);
1139
+ return this.asObject(r);
1140
+ }
1141
+ }
1142
+ // Serialize `where`/`sort` into the `q` query param (URL-encoded JSON), or
1143
+ // undefined when neither is given. URLSearchParams handles the percent-encoding.
1144
+ function buildListQueryParam(where, sort) {
1145
+ const q = {};
1146
+ if (where !== undefined && where.length > 0)
1147
+ q.where = where;
1148
+ if (sort !== undefined && sort.length > 0)
1149
+ q.sort = sort;
1150
+ if (q.where === undefined && q.sort === undefined)
1151
+ return undefined;
1152
+ return JSON.stringify(q);
760
1153
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { HomespunClient, HomespunApiError } from "./client.js";
2
- export type { ClientOptions, RelayResponse, AttachmentRef, UploadBlobOptions, PresignBlobOptions, AttachmentTokenMintResponse, ListBlobsOptions, AttachmentTokenAuditEntry, AttachmentTokenListResponse, QueryResponse, AppAsset, DeployAppRequest, DeployAppResponse, RedeployAppRequest, RedeployAppResponse, AppSummary, AppDetail, AppsPage, AppDomain, AppDomainDnsRecord, AppMember, AddAppMemberResult, AppRow, AppRowsPage, AppFeedEntry, AppFeedPage, } from "./client.js";
2
+ export type { ClientOptions, RelayResponse, AttachmentRef, UploadBlobOptions, PresignBlobOptions, AttachmentTokenMintResponse, ListBlobsOptions, AttachmentTokenAuditEntry, AttachmentTokenListResponse, QueryResponse, AppAsset, DeployAppRequest, DeployAppResponse, RedeployAppRequest, RedeployAppResponse, DeployCheckRequest, DeployCheckResult, KeyMintResult, AppSummary, AppDetail, AppsPage, AppDomain, AppDomainDnsRecord, AppMember, AddAppMemberResult, AppRow, AppRowsPage, ListScalar, ListWhereCondition, ListSortSpec, BatchRowInput, BatchRowResult, BatchResult, BatchWriteOptions, AppFeedEntry, AppFeedPage, CommunitySetupStep, } from "./client.js";
3
3
  export { openStream } from "./stream.js";
4
4
  export type { OpenStreamOptions, StreamHandlers, StreamHandle, } from "./stream.js";
5
5
  export { openAppStream, appWsUrlFromAppUrl } from "./app-stream.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/core",
3
- "version": "1.0.1",
3
+ "version": "1.4.3",
4
4
  "description": "Homespun relay client: typed HTTP + WebSocket operations against a Homespun relay. Framework-free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,9 +11,9 @@
11
11
  "relay",
12
12
  "human-in-the-loop"
13
13
  ],
14
- "homepage": "https://homespun.dev",
14
+ "homepage": "https://docs.homespun.dev",
15
15
  "bugs": {
16
- "url": "mailto:support@homespun.dev"
16
+ "url": "https://github.com/homespunapps/homespun/issues"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=20"
@@ -41,13 +41,18 @@
41
41
  "test:unit": "vitest run"
42
42
  },
43
43
  "dependencies": {
44
- "ws": "^8.20.1",
44
+ "ws": "^8.21.1",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^25.9.2",
48
+ "@types/node": "^26.1.1",
49
49
  "@types/ws": "^8.18.1",
50
- "typescript": "^6.0.3",
50
+ "typescript": "^7.0.2",
51
51
  "vitest": "^4.1.8"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/homespunapps/homespun.git",
56
+ "directory": "packages/core"
52
57
  }
53
58
  }