@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 +711 -4
- package/dist/client.js +401 -8
- package/dist/index.d.ts +1 -1
- package/package.json +11 -6
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
|
-
/**
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
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
|
-
/**
|
|
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.
|
|
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": "
|
|
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.
|
|
44
|
+
"ws": "^8.21.1",
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^
|
|
48
|
+
"@types/node": "^26.1.1",
|
|
49
49
|
"@types/ws": "^8.18.1",
|
|
50
|
-
"typescript": "^
|
|
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
|
}
|