@homespunapps/core 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -421,6 +421,34 @@ export declare class HomespunClient {
421
421
  url: string;
422
422
  };
423
423
  }>;
424
+ /**
425
+ * PUT /v1/apps/:id/ingest-hooks/:name/signing-secret provisions or rotates a
426
+ * hook's SIGNING secret (opt-in webhook signature verification, issue #935,
427
+ * shipped dark: nothing verifies a signature yet). This is a DIFFERENT secret
428
+ * from the URL secret rotateIngestHook rotates: it is what a provider (GitHub,
429
+ * Stripe, ...) HMACs the request body with.
430
+ *
431
+ * Omit `secret` to have the relay MINT one (`ihk_...`): the response includes
432
+ * the value ONCE (paste it into the provider). Pass `secret` to store a
433
+ * provider-generated value verbatim: the value is never echoed back, only the
434
+ * fingerprint. A rotation keeps the previous secret valid for `graceSeconds`
435
+ * (default 3600, max 86400) so deliveries verify while you update the provider.
436
+ */
437
+ setIngestSigningSecret(appId: string, name: string, opts?: {
438
+ secret?: string;
439
+ graceSeconds?: number;
440
+ }): Promise<{
441
+ secret?: string;
442
+ fingerprint: string;
443
+ setAt: string;
444
+ }>;
445
+ /**
446
+ * DELETE /v1/apps/:id/ingest-hooks/:name/signing-secret clears a hook's
447
+ * signing secret (current + previous + grace window). Idempotent.
448
+ */
449
+ clearIngestSigningSecret(appId: string, name: string): Promise<{
450
+ ok: boolean;
451
+ }>;
424
452
  /**
425
453
  * POST /v1/apps/:id/grants: mint a grant link. `role` must be a declared
426
454
  * custom role for the app. `mode` is "once" (one-time, first-browser-claims)
@@ -553,6 +581,22 @@ export declare class HomespunClient {
553
581
  decision: "approve" | "reject";
554
582
  note?: string;
555
583
  }): Promise<CommunitySubmissionDetail>;
584
+ /**
585
+ * GET /v1/community/templates/:ref/config-contract: the install-time config
586
+ * contract for an installable template (its settings collection + the ordered
587
+ * keyed config/upload steps). Use it to discover what a template needs and to
588
+ * pre-upload files for `upload` steps (POST /v1/attachments) before install.
589
+ */
590
+ getCommunityConfigContract(ref: string): Promise<CommunityConfigContract>;
591
+ /**
592
+ * POST /v1/community/templates/:ref/install: install a community template for
593
+ * the calling agent's owning human. `config` is `{ [stepKey]: value }` where a
594
+ * `config` step's value is a string and an `upload` step's value is an
595
+ * attachment id the agent pre-uploaded (agent-scoped) via `uploadBlob()` /
596
+ * `uploadBlobInline()`; the relay re-points it to the new app on install.
597
+ * Installs always create; returns the new app's id, slug, and url.
598
+ */
599
+ installCommunityTemplate(ref: string, config?: Record<string, unknown>): Promise<CommunityInstallResult>;
556
600
  /** GET /v1/publisher: the caller's own publisher profile. */
557
601
  getPublisher(): Promise<PublisherProfile>;
558
602
  /** POST /v1/publisher/claim: set the handle once (permanent after claiming). */
@@ -790,6 +834,20 @@ export interface IngestHookInfo {
790
834
  handshake: string | null;
791
835
  disabledAt: string | null;
792
836
  createdAt: string;
837
+ /**
838
+ * Opt-in signature verification state (issue #935), shipped dark. `configured`
839
+ * is true when a signing secret is set; `fingerprint` identifies WHICH one
840
+ * (plaintext-derived, matches the value in the provider's settings) without
841
+ * ever exposing it; `previousUntil` is set during a rotation's grace window.
842
+ */
843
+ signingSecret: {
844
+ configured: boolean;
845
+ fingerprint: string | null;
846
+ setAt: string | null;
847
+ previousUntil: string | null;
848
+ };
849
+ /** Placeholder for the D3 manifest `verify` grammar; always null for now. */
850
+ verify: null;
793
851
  deliveries: {
794
852
  accepted: number;
795
853
  failed: number;
@@ -1001,6 +1059,61 @@ export interface CommunitySetupStep {
1001
1059
  * (letters, digits, '_', up to 64 chars).
1002
1060
  */
1003
1061
  key?: string;
1062
+ /**
1063
+ * The manifest `ingest` rule this step wires up (D7). Allowed ONLY on a
1064
+ * `connect` step, optional there. Publish rejects a name the manifest does
1065
+ * not declare. After install, the installer pastes that rule's freshly
1066
+ * provisioned hook URL into the external service.
1067
+ */
1068
+ ingestRule?: string;
1069
+ }
1070
+ /**
1071
+ * One keyed config/upload step in a template's install-time config contract
1072
+ * (install-config programme, PR 4). Field names are the wire shape (snake_case
1073
+ * `value_hint`). An `upload` step's answer is an attachment id string.
1074
+ */
1075
+ export interface CommunityConfigStep {
1076
+ key: string;
1077
+ kind: "config" | "upload";
1078
+ label: string;
1079
+ description?: string;
1080
+ required: boolean;
1081
+ secret: boolean;
1082
+ choices?: string[];
1083
+ default?: string;
1084
+ value_hint?: string;
1085
+ }
1086
+ /**
1087
+ * One `connect` step in a template's contract that wires up an INBOUND hook:
1088
+ * after install, the app's own freshly provisioned hook URL for `ingest_rule`
1089
+ * has to be pasted into the external service the step describes. Read the URLs
1090
+ * from `GET /v1/apps/:app_id/ingest-hooks` once the install returns.
1091
+ */
1092
+ export interface CommunityConnectStep {
1093
+ label: string;
1094
+ description?: string;
1095
+ ingest_rule: string;
1096
+ /** Collection the hook writes into, or null when the rule went missing. */
1097
+ collection: string | null;
1098
+ mode: "append" | "upsert" | null;
1099
+ }
1100
+ /** The install-time config contract for an installable community template. */
1101
+ export interface CommunityConfigContract {
1102
+ snapshot_id: string;
1103
+ name: string;
1104
+ slug: string | null;
1105
+ /** The manifest's declared settings collection, or null when none. */
1106
+ settings_collection: string | null;
1107
+ /** Ordered keyed config/upload steps; empty when the template needs none. */
1108
+ config_steps: CommunityConfigStep[];
1109
+ /** Ordered connect steps that name an ingest rule; empty when none. */
1110
+ connect_steps: CommunityConnectStep[];
1111
+ }
1112
+ /** Result of an agent-key community install (installs always create). */
1113
+ export interface CommunityInstallResult {
1114
+ app_id: string;
1115
+ slug: string;
1116
+ url: string;
1004
1117
  }
1005
1118
  export interface PublishCommunityTemplateRequest {
1006
1119
  /** The owned app to publish. */
@@ -1149,6 +1262,26 @@ export interface CommunitySubmissionDetail extends CommunitySubmissionSummary {
1149
1262
  * the template declares no steps.
1150
1263
  */
1151
1264
  setup_steps: CommunitySetupStep[] | null;
1265
+ /**
1266
+ * Every external destination the manifest declares (D6 disclosure): outbound
1267
+ * webhook targets, the app's fetch allowance and its framing allowance,
1268
+ * deduplicated and HOSTS ONLY, so a reviewer sees where the app can send
1269
+ * data without reading the raw manifest. Empty when it talks to nothing
1270
+ * outside Homespun.
1271
+ */
1272
+ external_destinations: CommunityExternalDestination[];
1273
+ }
1274
+ /** One disclosed external destination on a submission (D6). */
1275
+ export interface CommunityExternalDestination {
1276
+ /** `webhook` = the relay POSTs row data there; `fetch` / `embed` = the app's
1277
+ * own page may call or frame it. */
1278
+ kind: "webhook" | "embed" | "fetch";
1279
+ /** Destination host, or null when the manifest alone cannot name it. */
1280
+ host: string | null;
1281
+ /** Settings field an install-time webhook target is read from. */
1282
+ fromSetting?: string;
1283
+ /** Stored connection a relative webhook target resolves against. */
1284
+ viaConnection?: string;
1152
1285
  }
1153
1286
  /** A publisher's own profile, as returned by the `/v1/publisher` routes. */
1154
1287
  export interface PublisherProfile {
package/dist/client.js CHANGED
@@ -675,9 +675,13 @@ export class HomespunClient {
675
675
  // (humanId/createdAt), matching packages/relay/src/http/routes/app-members.ts
676
676
  // exactly rather than the usual snake_case convention.
677
677
  //
678
- // Template `install`/`uninstall` is intentionally NOT wrapped here — that
679
- // route is `requireHuman` (human login cookie only), not agent-key
680
- // 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.
681
685
  // -------------------------------------------------------------------------
682
686
  /**
683
687
  * POST /v1/apps/:id/members — add or invite a member by email. If a Human
@@ -773,6 +777,40 @@ export class HomespunClient {
773
777
  this.fail(r);
774
778
  return this.asObject(r);
775
779
  }
780
+ /**
781
+ * PUT /v1/apps/:id/ingest-hooks/:name/signing-secret provisions or rotates a
782
+ * hook's SIGNING secret (opt-in webhook signature verification, issue #935,
783
+ * shipped dark: nothing verifies a signature yet). This is a DIFFERENT secret
784
+ * from the URL secret rotateIngestHook rotates: it is what a provider (GitHub,
785
+ * Stripe, ...) HMACs the request body with.
786
+ *
787
+ * Omit `secret` to have the relay MINT one (`ihk_...`): the response includes
788
+ * the value ONCE (paste it into the provider). Pass `secret` to store a
789
+ * provider-generated value verbatim: the value is never echoed back, only the
790
+ * fingerprint. A rotation keeps the previous secret valid for `graceSeconds`
791
+ * (default 3600, max 86400) so deliveries verify while you update the provider.
792
+ */
793
+ async setIngestSigningSecret(appId, name, opts = {}) {
794
+ const body = {};
795
+ if (opts.secret !== undefined)
796
+ body.secret = opts.secret;
797
+ if (opts.graceSeconds !== undefined)
798
+ body.graceSeconds = opts.graceSeconds;
799
+ const r = await this.call("PUT", `/v1/apps/${encodeURIComponent(appId)}/ingest-hooks/${encodeURIComponent(name)}/signing-secret`, body);
800
+ if (!r.ok)
801
+ this.fail(r);
802
+ return this.asObject(r);
803
+ }
804
+ /**
805
+ * DELETE /v1/apps/:id/ingest-hooks/:name/signing-secret clears a hook's
806
+ * signing secret (current + previous + grace window). Idempotent.
807
+ */
808
+ async clearIngestSigningSecret(appId, name) {
809
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/ingest-hooks/${encodeURIComponent(name)}/signing-secret`);
810
+ if (!r.ok)
811
+ this.fail(r);
812
+ return this.asObject(r);
813
+ }
776
814
  // -------------------------------------------------------------------------
777
815
  // v2 grant links (M5). An owner/agent mints a capability URL carrying a
778
816
  // DECLARED custom role (x-homespun-manifest.roles) plus an optional row/filter
@@ -1012,6 +1050,38 @@ export class HomespunClient {
1012
1050
  return this.asObject(r);
1013
1051
  }
1014
1052
  // -------------------------------------------------------------------------
1053
+ // Agent-key community install (install-config programme, PR 4). Both act AS
1054
+ // the calling agent's owning human, who becomes the installed app's owner. A
1055
+ // `ref` is a namespaced `<handle>/<slug>` OR a community snapshot id;
1056
+ // encodeURIComponent keeps a namespaced ref one path segment on the wire.
1057
+ // -------------------------------------------------------------------------
1058
+ /**
1059
+ * GET /v1/community/templates/:ref/config-contract: the install-time config
1060
+ * contract for an installable template (its settings collection + the ordered
1061
+ * keyed config/upload steps). Use it to discover what a template needs and to
1062
+ * pre-upload files for `upload` steps (POST /v1/attachments) before install.
1063
+ */
1064
+ async getCommunityConfigContract(ref) {
1065
+ const r = await this.call("GET", `/v1/community/templates/${encodeURIComponent(ref)}/config-contract`);
1066
+ if (!r.ok)
1067
+ this.fail(r);
1068
+ return this.asObject(r);
1069
+ }
1070
+ /**
1071
+ * POST /v1/community/templates/:ref/install: install a community template for
1072
+ * the calling agent's owning human. `config` is `{ [stepKey]: value }` where a
1073
+ * `config` step's value is a string and an `upload` step's value is an
1074
+ * attachment id the agent pre-uploaded (agent-scoped) via `uploadBlob()` /
1075
+ * `uploadBlobInline()`; the relay re-points it to the new app on install.
1076
+ * Installs always create; returns the new app's id, slug, and url.
1077
+ */
1078
+ async installCommunityTemplate(ref, config) {
1079
+ const r = await this.call("POST", `/v1/community/templates/${encodeURIComponent(ref)}/install`, config !== undefined ? { config } : {});
1080
+ if (!r.ok)
1081
+ this.fail(r);
1082
+ return this.asObject(r);
1083
+ }
1084
+ // -------------------------------------------------------------------------
1015
1085
  // Community publisher identity (marketplace PR 1). All three act AS the
1016
1086
  // calling agent's owning human. claim/update additionally need a verified
1017
1087
  // email server-side.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/core",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Homespun relay client: typed HTTP + WebSocket operations against a Homespun relay. Framework-free.",
5
5
  "license": "MIT",
6
6
  "type": "module",