@homespunapps/core 1.6.43 → 1.6.45

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.
@@ -40,6 +40,10 @@ export interface AppStreamHandlers {
40
40
  onResync?: () => void;
41
41
  /** Fired on the terminal `_dormant` frame (the app went dormant). */
42
42
  onDormant?: () => void;
43
+ /** Fired on the terminal `_suspended` frame (an operator suspended the
44
+ * app, issue #1041). Distinct from `onDormant`: there is no self-service
45
+ * recovery here, only an operator can unsuspend. */
46
+ onSuspended?: () => void;
43
47
  /** Fired on a relay-side error frame. */
44
48
  onRelayError?: (error: {
45
49
  code?: string;
@@ -12,6 +12,7 @@
12
12
  // server -> client {"type":"entry","entry":{...}} (live push)
13
13
  // server -> client {"type":"resync"} (since below retention floor)
14
14
  // server -> client {"type":"_dormant"} (terminal — app went dormant)
15
+ // server -> client {"type":"_suspended"} (terminal: operator takedown, #1041)
15
16
  // server -> client {"type":"error","error":{...}}
16
17
  //
17
18
  // `openAppStream` sends the initial `sub` itself (from `opts.since`) and
@@ -94,6 +95,9 @@ export function openAppStream(opts, handlers) {
94
95
  case "_dormant":
95
96
  handlers.onDormant?.();
96
97
  return;
98
+ case "_suspended":
99
+ handlers.onSuspended?.();
100
+ return;
97
101
  case "error":
98
102
  handlers.onRelayError?.(obj["error"]);
99
103
  return;
package/dist/client.d.ts CHANGED
@@ -313,7 +313,7 @@ export declare class HomespunClient {
313
313
  * human-given slug to an id (see the class-level note above).
314
314
  */
315
315
  listApps(opts?: {
316
- status?: "active" | "dormant" | "archived" | "all";
316
+ status?: "active" | "dormant" | "archived" | "suspended" | "all";
317
317
  limit?: number;
318
318
  cursor?: string;
319
319
  slug?: string;
@@ -553,6 +553,99 @@ export declare class HomespunClient {
553
553
  }>;
554
554
  /** DELETE /v1/apps/:id/grants/:grantId: revoke one grant link (idempotent). */
555
555
  revokeAppGrant(appId: string, grantId: string): Promise<void>;
556
+ /**
557
+ * POST /v1/apps/:id/credentials: mint a service credential scoped to the
558
+ * named collections and operations. `mode` defaults to "explicit" (an
559
+ * unnamed collection is denied); "following" tracks the app as it grows,
560
+ * with each `grants` entry narrowing one collection. `ttlSeconds: null` is
561
+ * the explicit opt-in to NO EXPIRY; omit for the server's bounded default.
562
+ * Returns the raw `token` once: only its hash is stored.
563
+ */
564
+ mintAppCredential(appId: string, opts?: {
565
+ mode?: "explicit" | "following";
566
+ grants?: ServiceCredentialGrant[];
567
+ members?: boolean;
568
+ label?: string;
569
+ ttlSeconds?: number | null;
570
+ }): Promise<MintAppCredentialResult>;
571
+ /**
572
+ * GET /v1/apps/:id/credentials: list the app's service credentials with
573
+ * their allowlist and status. Never any token material beyond the display
574
+ * prefix, which is not an authenticator.
575
+ */
576
+ listAppCredentials(appId: string): Promise<{
577
+ credentials: AppCredentialSummary[];
578
+ }>;
579
+ /**
580
+ * POST /v1/apps/:id/credentials/:credentialId/pause: reversible stop, in
581
+ * force on the credential's very next request. Idempotent.
582
+ */
583
+ pauseAppCredential(appId: string, credentialId: string): Promise<void>;
584
+ /**
585
+ * POST /v1/apps/:id/credentials/:credentialId/resume: undo a pause. Never
586
+ * undoes a revoke, which is permanent. Idempotent.
587
+ */
588
+ resumeAppCredential(appId: string, credentialId: string): Promise<void>;
589
+ /**
590
+ * POST /v1/apps/:id/credentials/:credentialId/rotate: issue a fresh token
591
+ * and keep the superseded one resolving for `overlapSeconds` (default the
592
+ * server's own default) so a running backend can pick up the new one
593
+ * without a gap. Returns the new raw token once, exactly as mint does.
594
+ */
595
+ rotateAppCredential(appId: string, credentialId: string, opts?: {
596
+ overlapSeconds?: number;
597
+ }): Promise<RotateAppCredentialResult>;
598
+ /**
599
+ * DELETE /v1/apps/:id/credentials/:credentialId: revoke one credential
600
+ * permanently. Also kills any superseded token still inside a rotation
601
+ * overlap window. Idempotent.
602
+ */
603
+ revokeAppCredential(appId: string, credentialId: string): Promise<void>;
604
+ /**
605
+ * POST /v1/apps/:id/connections: create a connection. `kind` defaults to
606
+ * "static" (name + allowedHost + headerName + headerValue). For
607
+ * `kind: "oauth2"` the caller supplies the WHOLE provider config
608
+ * (authorizeUrl, tokenEndpoint, clientId, clientSecret, allowedHost, and
609
+ * the optional scopes/authScheme/instanceField/authParams/tokenParams);
610
+ * the row starts in `pending_auth` until the signed-in OWNER completes
611
+ * consent in a browser at `connectionAuthorizeUrl` (an agent key cannot
612
+ * complete OAuth consent). Every stored secret is encrypted at rest and
613
+ * never returned by any call.
614
+ */
615
+ createConnection(appId: string, body: {
616
+ name: string;
617
+ kind?: "static" | "oauth2";
618
+ provider?: string;
619
+ label?: string;
620
+ allowedHost: string;
621
+ headerName?: string;
622
+ headerValue?: string;
623
+ authorizeUrl?: string;
624
+ tokenEndpoint?: string;
625
+ clientId?: string;
626
+ clientSecret?: string;
627
+ scopes?: string;
628
+ authScheme?: string;
629
+ instanceField?: string;
630
+ authParams?: Record<string, unknown>;
631
+ tokenParams?: Record<string, unknown>;
632
+ }): Promise<{
633
+ connection: ConnectionSummary;
634
+ }>;
635
+ /** GET /v1/apps/:id/connections: list metadata only, never the secret. */
636
+ listConnections(appId: string): Promise<{
637
+ connections: ConnectionSummary[];
638
+ }>;
639
+ /** DELETE /v1/apps/:id/connections/:name: idempotent. */
640
+ deleteConnection(appId: string, name: string): Promise<void>;
641
+ /**
642
+ * The browser URL that completes an oauth2 connection's consent. Build
643
+ * only, never fetch this yourself: it 302s the caller to the third-party
644
+ * provider, and the relay refuses an agent-key caller here (consent is
645
+ * inherently a human-in-a-browser step). Hand the URL to the signed-in
646
+ * owner to open.
647
+ */
648
+ connectionAuthorizeUrl(appId: string, name: string): string;
556
649
  /**
557
650
  * GET /v1/apps/:id/collections/:name — list rows (current-state page).
558
651
  *
@@ -1116,6 +1209,71 @@ export interface AppGrantSummary {
1116
1209
  revoked_at: string | null;
1117
1210
  created_at: string;
1118
1211
  }
1212
+ /** One collection entry in a service credential's allowlist. */
1213
+ export interface ServiceCredentialGrant {
1214
+ collection: string;
1215
+ ops: ("read" | "create" | "update" | "delete")[];
1216
+ /** "own" narrows every row-addressed op to rows this credential wrote last. */
1217
+ scope?: "own";
1218
+ }
1219
+ /** `POST /v1/apps/:id/credentials` response. `token` is shown ONCE. */
1220
+ export interface MintAppCredentialResult {
1221
+ id: string;
1222
+ token: string;
1223
+ token_prefix: string;
1224
+ mode: string;
1225
+ grants: ServiceCredentialGrant[];
1226
+ members: boolean;
1227
+ label: string | null;
1228
+ expires_at: string | null;
1229
+ /** Present + true iff minting cleared the app's egress waiver (#1328). */
1230
+ unrestricted_revoked?: true;
1231
+ unrestricted_notice?: string;
1232
+ }
1233
+ /** One service credential row from `listAppCredentials` (never any token). */
1234
+ export interface AppCredentialSummary {
1235
+ id: string;
1236
+ label: string | null;
1237
+ token_prefix: string;
1238
+ mode: string;
1239
+ grants: ServiceCredentialGrant[];
1240
+ members: boolean;
1241
+ paused: boolean;
1242
+ active: boolean;
1243
+ expires_at: string | null;
1244
+ paused_at: string | null;
1245
+ revoked_at: string | null;
1246
+ rotated_at: string | null;
1247
+ previous_token_prefix: string | null;
1248
+ previous_expires_at: string | null;
1249
+ created_at: string;
1250
+ last_used_at: string | null;
1251
+ }
1252
+ /** `POST /v1/apps/:id/credentials/:credentialId/rotate` response. */
1253
+ export interface RotateAppCredentialResult {
1254
+ id: string;
1255
+ token: string;
1256
+ token_prefix: string;
1257
+ /** The instant the SUPERSEDED token stops resolving. Both work until then. */
1258
+ previous_expires_at: string;
1259
+ }
1260
+ /** One connection row (never any secret, metadata plus a fingerprint). */
1261
+ export interface ConnectionSummary {
1262
+ id: string;
1263
+ name: string;
1264
+ kind: string;
1265
+ provider: string | null;
1266
+ label: string | null;
1267
+ allowedHost: string | null;
1268
+ headerName: string | null;
1269
+ status: string;
1270
+ authScheme: string | null;
1271
+ instanceBaseUrl: string | null;
1272
+ scopes: string | null;
1273
+ expiresAt: string | null;
1274
+ secretFingerprint: string | null;
1275
+ createdAt: string;
1276
+ }
1119
1277
  /** One row in an app collection — the shape every row CRUD op returns. */
1120
1278
  export interface AppRow {
1121
1279
  key: string;
package/dist/client.js CHANGED
@@ -968,6 +968,132 @@ export class HomespunClient {
968
968
  this.fail(r);
969
969
  }
970
970
  // -------------------------------------------------------------------------
971
+ // Scoped service credentials (#1354, #1355, #1363). The bearer token an app
972
+ // owner points a backend they host themselves at, carrying an explicitly
973
+ // enumerated SUBSET of the owner's authority and bound to one app. Shaped on
974
+ // the grant-link methods above: mint's raw token is returned exactly ONCE
975
+ // and is never retrievable afterward, list never returns any token
976
+ // material, and every verb is owner-or-owning-agent only at the relay (a
977
+ // credential itself can reach none of these).
978
+ // -------------------------------------------------------------------------
979
+ /**
980
+ * POST /v1/apps/:id/credentials: mint a service credential scoped to the
981
+ * named collections and operations. `mode` defaults to "explicit" (an
982
+ * unnamed collection is denied); "following" tracks the app as it grows,
983
+ * with each `grants` entry narrowing one collection. `ttlSeconds: null` is
984
+ * the explicit opt-in to NO EXPIRY; omit for the server's bounded default.
985
+ * Returns the raw `token` once: only its hash is stored.
986
+ */
987
+ async mintAppCredential(appId, opts = {}) {
988
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/credentials`, {
989
+ mode: opts.mode,
990
+ grants: opts.grants,
991
+ members: opts.members,
992
+ label: opts.label,
993
+ ttl_seconds: opts.ttlSeconds,
994
+ });
995
+ if (!r.ok)
996
+ this.fail(r);
997
+ return this.asObject(r);
998
+ }
999
+ /**
1000
+ * GET /v1/apps/:id/credentials: list the app's service credentials with
1001
+ * their allowlist and status. Never any token material beyond the display
1002
+ * prefix, which is not an authenticator.
1003
+ */
1004
+ async listAppCredentials(appId) {
1005
+ const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/credentials`);
1006
+ if (!r.ok)
1007
+ this.fail(r);
1008
+ return this.asObject(r);
1009
+ }
1010
+ /**
1011
+ * POST /v1/apps/:id/credentials/:credentialId/pause: reversible stop, in
1012
+ * force on the credential's very next request. Idempotent.
1013
+ */
1014
+ async pauseAppCredential(appId, credentialId) {
1015
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/credentials/${encodeURIComponent(credentialId)}/pause`);
1016
+ if (!r.ok)
1017
+ this.fail(r);
1018
+ }
1019
+ /**
1020
+ * POST /v1/apps/:id/credentials/:credentialId/resume: undo a pause. Never
1021
+ * undoes a revoke, which is permanent. Idempotent.
1022
+ */
1023
+ async resumeAppCredential(appId, credentialId) {
1024
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/credentials/${encodeURIComponent(credentialId)}/resume`);
1025
+ if (!r.ok)
1026
+ this.fail(r);
1027
+ }
1028
+ /**
1029
+ * POST /v1/apps/:id/credentials/:credentialId/rotate: issue a fresh token
1030
+ * and keep the superseded one resolving for `overlapSeconds` (default the
1031
+ * server's own default) so a running backend can pick up the new one
1032
+ * without a gap. Returns the new raw token once, exactly as mint does.
1033
+ */
1034
+ async rotateAppCredential(appId, credentialId, opts = {}) {
1035
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/credentials/${encodeURIComponent(credentialId)}/rotate`, { overlap_seconds: opts.overlapSeconds });
1036
+ if (!r.ok)
1037
+ this.fail(r);
1038
+ return this.asObject(r);
1039
+ }
1040
+ /**
1041
+ * DELETE /v1/apps/:id/credentials/:credentialId: revoke one credential
1042
+ * permanently. Also kills any superseded token still inside a rotation
1043
+ * overlap window. Idempotent.
1044
+ */
1045
+ async revokeAppCredential(appId, credentialId) {
1046
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/credentials/${encodeURIComponent(credentialId)}`);
1047
+ if (!r.ok)
1048
+ this.fail(r);
1049
+ }
1050
+ // -------------------------------------------------------------------------
1051
+ // Connections (#1363): the stored credential a webhook rule authenticates
1052
+ // its target with (a static header token, or a full OAuth2 client), bound
1053
+ // to a host so it can never be exfiltrated to another one. There is no
1054
+ // update verb: change a connection by deleting and recreating it.
1055
+ // -------------------------------------------------------------------------
1056
+ /**
1057
+ * POST /v1/apps/:id/connections: create a connection. `kind` defaults to
1058
+ * "static" (name + allowedHost + headerName + headerValue). For
1059
+ * `kind: "oauth2"` the caller supplies the WHOLE provider config
1060
+ * (authorizeUrl, tokenEndpoint, clientId, clientSecret, allowedHost, and
1061
+ * the optional scopes/authScheme/instanceField/authParams/tokenParams);
1062
+ * the row starts in `pending_auth` until the signed-in OWNER completes
1063
+ * consent in a browser at `connectionAuthorizeUrl` (an agent key cannot
1064
+ * complete OAuth consent). Every stored secret is encrypted at rest and
1065
+ * never returned by any call.
1066
+ */
1067
+ async createConnection(appId, body) {
1068
+ const r = await this.call("POST", `/v1/apps/${encodeURIComponent(appId)}/connections`, body);
1069
+ if (!r.ok)
1070
+ this.fail(r);
1071
+ return this.asObject(r);
1072
+ }
1073
+ /** GET /v1/apps/:id/connections: list metadata only, never the secret. */
1074
+ async listConnections(appId) {
1075
+ const r = await this.call("GET", `/v1/apps/${encodeURIComponent(appId)}/connections`);
1076
+ if (!r.ok)
1077
+ this.fail(r);
1078
+ return this.asObject(r);
1079
+ }
1080
+ /** DELETE /v1/apps/:id/connections/:name: idempotent. */
1081
+ async deleteConnection(appId, name) {
1082
+ const r = await this.call("DELETE", `/v1/apps/${encodeURIComponent(appId)}/connections/${encodeURIComponent(name)}`);
1083
+ if (!r.ok)
1084
+ this.fail(r);
1085
+ }
1086
+ /**
1087
+ * The browser URL that completes an oauth2 connection's consent. Build
1088
+ * only, never fetch this yourself: it 302s the caller to the third-party
1089
+ * provider, and the relay refuses an agent-key caller here (consent is
1090
+ * inherently a human-in-a-browser step). Hand the URL to the signed-in
1091
+ * owner to open.
1092
+ */
1093
+ connectionAuthorizeUrl(appId, name) {
1094
+ return `${this.baseUrl}/v1/apps/${encodeURIComponent(appId)}/connections/${encodeURIComponent(name)}/authorize`;
1095
+ }
1096
+ // -------------------------------------------------------------------------
971
1097
  // v2 collection row CRUD + feed catch-up (spec-cli §2.2/§2.3). Same wire
972
1098
  // shapes as the v1 homespun-records methods above, mounted under
973
1099
  // /v1/apps/:id/collections/:name instead of /v1/apps/:id/records/:name.
@@ -0,0 +1 @@
1
+ export declare const RELAY_FAILURE_REPORT_HINT = "this failure is homespun's, not a bad argument. If it is not already in your feedback list, file it once as type `bug`, with the error code above, what you expected, and the arguments you passed.";
@@ -0,0 +1,14 @@
1
+ // The one sentence that turns a relay-side failure into a filed report.
2
+ //
3
+ // Both agent-facing surfaces attach this to errors that are homespun's fault:
4
+ // the MCP tool result (packages/mcp/src/tools.ts) and the CLI error envelope
5
+ // (packages/cli/src/output.ts). It lives here so the two cannot drift into
6
+ // saying different things about the same channel.
7
+ //
8
+ // Attached ONLY to 5xx, never to a 4xx. A 4xx is normally the caller's own bad
9
+ // argument, and prompting a report on those would bury the real signal under
10
+ // rows the operator has to triage and close. The deliberate exception is a
11
+ // misleading error message, which the guide's "do not file" list calls out as a
12
+ // documentation bug worth a `note`, and that one needs the agent's judgement, so
13
+ // no automatic hint can catch it.
14
+ export const RELAY_FAILURE_REPORT_HINT = "this failure is homespun's, not a bad argument. If it is not already in your feedback list, file it once as type `bug`, with the error code above, what you expected, and the arguments you passed.";
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, DeployCheckRequest, DeployCheckResult, KeyMintResult, AppSummary, AppDetail, AppsPage, AppDomain, AppDomainDnsRecord, AppMember, AddAppMemberResult, AppRow, AppRowsPage, ListScalar, ListWhereCondition, ListSortSpec, BatchRowInput, BatchRowResult, BatchResult, BatchWriteOptions, AppFeedEntry, AppFeedPage, CommunitySetupStep, RetentionAxes, CollectionRetentionView, } 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, RetentionAxes, CollectionRetentionView, ServiceCredentialGrant, MintAppCredentialResult, AppCredentialSummary, RotateAppCredentialResult, ConnectionSummary, } 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";
@@ -7,6 +7,7 @@ export type { OpenAppStreamOptions, AppStreamHandlers, AppStreamHandle, } from "
7
7
  export { registerAgent } from "./register.js";
8
8
  export type { RegisterAgentOptions, RegisterAgentResult } from "./register.js";
9
9
  export { feedbackTypeSchema, submitFeedbackSchema } from "./schemas.js";
10
+ export { RELAY_FAILURE_REPORT_HINT } from "./feedback-prompt.js";
10
11
  export { validateIconEmoji, isValidIconEmoji, isRasterImageMime, RASTER_ICON_MIME_ALLOWLIST, MAX_ICON_EMOJI_BYTES, } from "./icons.js";
11
12
  export type { RasterIconMime } from "./icons.js";
12
13
  export { MAX_EVENT_TYPE_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, MAX_RESPONSE_SNIPPET_LENGTH, MAX_FRAME_SNIPPET_LENGTH, } from "./limits.js";
package/dist/index.js CHANGED
@@ -5,5 +5,6 @@ export { openStream } from "./stream.js";
5
5
  export { openAppStream, appWsUrlFromAppUrl } from "./app-stream.js";
6
6
  export { registerAgent } from "./register.js";
7
7
  export { feedbackTypeSchema, submitFeedbackSchema } from "./schemas.js";
8
+ export { RELAY_FAILURE_REPORT_HINT } from "./feedback-prompt.js";
8
9
  export { validateIconEmoji, isValidIconEmoji, isRasterImageMime, RASTER_ICON_MIME_ALLOWLIST, MAX_ICON_EMOJI_BYTES, } from "./icons.js";
9
10
  export { MAX_EVENT_TYPE_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, MAX_RESPONSE_SNIPPET_LENGTH, MAX_FRAME_SNIPPET_LENGTH, } from "./limits.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/core",
3
- "version": "1.6.43",
3
+ "version": "1.6.45",
4
4
  "description": "Homespun relay client: typed HTTP + WebSocket operations against a Homespun relay. Framework-free.",
5
5
  "license": "MIT",
6
6
  "type": "module",