@pithy-sh/support 0.1.5 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAGnE,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,0CAA0C,CAAC;AAC7F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EAGxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAO9E,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,+FAA+F;AAC/F,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,EAAE,eAAe,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD;;;;;;;;OAQG;IACH,IAAI,EAAE,SAAS,CAAC;CACjB;AA4ED;;;;;GAKG;AACH,MAAM,WAAW,iBACf,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,gBAAgB,CAAC;IAC3F,mCAAmC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,wGAAwG;IACxG,UAAU,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CA+FvE;AAED,2GAA2G;AAC3G,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,iBAAiB,CAE3F"}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAGnE,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,0CAA0C,CAAC;AAC7F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAItE,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EAGxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAO9E,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,+FAA+F;AAC/F,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,aAAa,CAAC;IACtB,8BAA8B;IAC9B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,EAAE,eAAe,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD;;;;;;;;OAQG;IACH,IAAI,EAAE,SAAS,CAAC;CACjB;AA4ED;;;;;GAKG;AACH,MAAM,WAAW,iBACf,SAAQ,UAAU,CAAC,eAAe,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,gBAAgB,CAAC;IAC3F,mCAAmC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,wGAAwG;IACxG,UAAU,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAmGvE;AAED,2GAA2G;AAC3G,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,iBAAiB,CAE3F"}
@@ -4,7 +4,7 @@ import { resolveReplies } from "./reply/snippets.js";
4
4
  import { SupportConfig, supportNeedsBucket } from "./config/config.js";
5
5
  import { resolveCategories } from "./data/categories.js";
6
6
  import { supportTables } from "./data/tables.js";
7
- import { supportSecretsRegistry } from "./secret/registry.js";
7
+ import { SUPPORT_BUCKET_BINDING, inapplicableAttachmentSecrets, supportSecretsRegistry } from "./secret/registry.js";
8
8
  import { supportWorkflows } from "./workflows/specs.js";
9
9
  import { makeResolveDeps } from "./http/resolve.js";
10
10
  import { supportAdminRoutes } from "./http/scopes.js";
@@ -133,7 +133,7 @@ function support(options = {}) {
133
133
  },
134
134
  ...supportNeedsBucket(resolved) ? [{
135
135
  type: "r2",
136
- name: "SUPPORT_BUCKET",
136
+ name: SUPPORT_BUCKET_BINDING,
137
137
  optional: true
138
138
  }] : [],
139
139
  ...workflowBindings(supportWorkflows)
@@ -143,6 +143,7 @@ function support(options = {}) {
143
143
  version: PACKAGE_VERSION,
144
144
  dependsOn: ["secrets"],
145
145
  secretRegistry: supportSecretsRegistry,
146
+ inapplicableSecrets: inapplicableAttachmentSecrets(resolved),
146
147
  workflows: supportWorkflows,
147
148
  requiredBindings,
148
149
  ciPermissions: ["email:routing"],
@@ -120,9 +120,13 @@ export interface SupportProvisionResult {
120
120
  * Worker whose classification host is not deployed yet, which is a window where real customer
121
121
  * messages arrive and stay `uncategorized` with nothing to say why.
122
122
  *
123
- * `environments` is the project's declaration from the root `pithy.config.ts` (#241). Every declared
124
- * environment is provisioned; an environment this skipped would be one the project deploys to with no
125
- * resources behind it — the silence the closed `ManagedEnvironment` enum used to produce.
123
+ * `environments` is **what the caller determined it can act on**, which the CLI narrows from the project's
124
+ * declaration (#241) to the environments whose app database exists (pithy-sh/pithy#512). Nothing is
125
+ * skipped here and nothing decides here: an environment reaching this list is provisioned, and one that
126
+ * did not was already reported to the operator by name, with why and with the command that fixes it.
127
+ *
128
+ * The bucket is created **whatever the list holds, including nothing** — it is one per project, and making
129
+ * it wait for the last environment to be provisioned is the ordering this change exists to undo.
126
130
  */
127
131
  export declare function provisionSupport(provisioner: SupportProvisioner, environments: DeclaredEnvironments | readonly string[]): Promise<SupportProvisionResult>;
128
132
  /** The teardown seam — the inverse of {@link SupportProvisioner}. Every step idempotent. */
@@ -1 +1 @@
1
- {"version":3,"file":"provisionSupport.d.ts","sourceRoot":"","sources":["../../src/provision/provisionSupport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAIlF,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,6BAA6B,CAAC;AAG3F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAElF;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,YAAY,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,sEAAsE;IACtE,YAAY,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;;;;;;OAQG;IACH,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F;;;OAGG;IACH,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,kCAAkC;AAClC,MAAM,WAAW,sBAAsB;IACrC,uDAAuD;IACvD,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/D,iEAAiE;IACjE,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,4FAA4F;IAC5F,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,kBAAkB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC/E,iFAAiF;IACjF,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACjD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,kBAAkB,EAC/B,YAAY,EAAE,oBAAoB,GAAG,SAAS,MAAM,EAAE,GACrD,OAAO,CAAC,sBAAsB,CAAC,CAYjC;AAED,4FAA4F;AAC5F,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,YAAY,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,uFAAuF;IACvF,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,+FAA+F;AAC/F,MAAM,WAAW,yBAAyB;IACxC,kEAAkE;IAClE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,oBAAoB,EACnC,YAAY,EAAE,oBAAoB,GAAG,SAAS,MAAM,EAAE,EACtD,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CAMf"}
1
+ {"version":3,"file":"provisionSupport.d.ts","sourceRoot":"","sources":["../../src/provision/provisionSupport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAIlF,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,6BAA6B,CAAC;AAG3F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAElF;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B;;;;;OAKG;IACH,YAAY,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChF,sEAAsE;IACtE,YAAY,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;;;;;;OAQG;IACH,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F;;;OAGG;IACH,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,kCAAkC;AAClC,MAAM,WAAW,sBAAsB;IACrC,uDAAuD;IACvD,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/D,iEAAiE;IACjE,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,4FAA4F;IAC5F,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,kBAAkB,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC/E,iFAAiF;IACjF,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CACjD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,kBAAkB,EAC/B,YAAY,EAAE,oBAAoB,GAAG,SAAS,MAAM,EAAE,GACrD,OAAO,CAAC,sBAAsB,CAAC,CAiBjC;AAED,4FAA4F;AAC5F,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,YAAY,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,uFAAuF;IACvF,iBAAiB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,+FAA+F;AAC/F,MAAM,WAAW,yBAAyB;IACxC,kEAAkE;IAClE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,aAAa,EAAE,oBAAoB,EACnC,YAAY,EAAE,oBAAoB,GAAG,SAAS,MAAM,EAAE,EACtD,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,IAAI,CAAC,CAMf"}
@@ -68,26 +68,33 @@ function supportWorkerName(project, env) {
68
68
  * Worker whose classification host is not deployed yet, which is a window where real customer
69
69
  * messages arrive and stay `uncategorized` with nothing to say why.
70
70
  *
71
- * `environments` is the project's declaration from the root `pithy.config.ts` (#241). Every declared
72
- * environment is provisioned; an environment this skipped would be one the project deploys to with no
73
- * resources behind it — the silence the closed `ManagedEnvironment` enum used to produce.
71
+ * `environments` is **what the caller determined it can act on**, which the CLI narrows from the project's
72
+ * declaration (#241) to the environments whose app database exists (pithy-sh/pithy#512). Nothing is
73
+ * skipped here and nothing decides here: an environment reaching this list is provisioned, and one that
74
+ * did not was already reported to the operator by name, with why and with the command that fixes it.
75
+ *
76
+ * The bucket is created **whatever the list holds, including nothing** — it is one per project, and making
77
+ * it wait for the last environment to be provisioned is the ordering this change exists to undo.
74
78
  */
75
79
  async function provisionSupport(provisioner, environments) {
76
80
  await provisioner.preflight();
77
81
  const bucket = await provisioner.ensureBucket();
78
82
  const search = [];
79
- for (const env of managedEnvironments(environments)) {
83
+ const deployed = managedEnvironments(environments);
84
+ for (const env of deployed) {
80
85
  await provisioner.deployWorker(env);
81
86
  search.push({
82
87
  env,
83
88
  ...await provisioner.ensureSearchIndex(env)
84
89
  });
85
90
  }
86
- const routing = await provisioner.ensureRoutingRule();
87
91
  return {
88
92
  bucket,
89
- environments: managedEnvironments(environments),
90
- routing,
93
+ environments: deployed,
94
+ routing: deployed.length > 0 ? await provisioner.ensureRoutingRule() : {
95
+ created: false,
96
+ skipped: true
97
+ },
91
98
  search
92
99
  };
93
100
  }
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import { type SupportConfig } from "../config/config";
3
4
  /**
4
5
  * The secrets support reads — exactly one, and it is not support's own shape.
5
6
  *
@@ -15,6 +16,35 @@
15
16
  */
16
17
  /** The name support's R2 credential bundle is stored and resolved under, per environment. */
17
18
  export declare const SUPPORT_R2_SECRET = "support-r2-credentials";
19
+ /**
20
+ * The bucket binding it presigns — support's one optional binding, and the one an adopter declines when
21
+ * attachments are off.
22
+ *
23
+ * **Declared, because the relation was prose and a command cannot read prose (#541).** With attachments
24
+ * declined, `pithy doctor` printed `SUPPORT_BUCKET (r2) declined in pithy.config.ts` and then asked for
25
+ * `support-r2-credentials` in the same run. The binding is stated on the entry now, so the secrets side
26
+ * reads the decline the bindings side already reports.
27
+ */
28
+ export declare const SUPPORT_BUCKET_BINDING = "SUPPORT_BUCKET";
18
29
  /** The support capability's secret-registry slice — aggregated into the shared accessor at startup. */
19
30
  export declare const supportSecretsRegistry: Record<"support-r2-credentials", import("@pithy-sh/storage/src/secret/registry").R2CredentialsEntry>;
31
+ /**
32
+ * **The credential this composition cannot reach**, as the `inapplicableSecrets` contribution (#541).
33
+ *
34
+ * The headline case of that issue, and the one the binding field alone does not close. Joining a
35
+ * credential to a **declined** binding only answers for a project that wrote a `declinedBindings` entry;
36
+ * a project that simply turned attachments off never wrote one, because the binding was never declared
37
+ * to decline — {@link supportNeedsBucket} suppresses it at composition. So `pithy doctor` kept asking
38
+ * for `support-r2-credentials` on exactly the configuration the issue opens with.
39
+ *
40
+ * **The predicate already existed.** Support has answered *would this configuration ever put a byte in
41
+ * `SUPPORT_BUCKET`* since #440, in one place, for the binding and the provisioner both. This is the
42
+ * third caller of the same answer rather than a fourth reading of the three settings behind it — which
43
+ * is the arrangement #440 landed precisely so that two callers could not disagree.
44
+ *
45
+ * The reason names the settings an operator would edit, all three of them, because any one of them
46
+ * turns the bucket back on and a sentence naming only `attachments.enabled` would send a reader to the
47
+ * wrong line.
48
+ */
49
+ export declare function inapplicableAttachmentSecrets(config: SupportConfig): Record<string, string>;
20
50
  //# sourceMappingURL=registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/secret/registry.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D,uGAAuG;AACvG,eAAO,MAAM,sBAAsB,sGAA2C,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/secret/registry.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,kBAAkB,CAAC;AAE1E;;;;;;;;;;;;GAYG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,2BAA2B,CAAC;AAE1D;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,mBAAmB,CAAC;AAEvD,uGAAuG;AACvG,eAAO,MAAM,sBAAsB,sGAAmE,CAAC;AAEvG;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM3F"}
@@ -1,5 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import { supportNeedsBucket } from "../config/config.js";
3
4
  import { r2CredentialsRegistry } from "@pithy-sh/storage/src/secret/registry";
4
5
  //#region src/secret/registry.ts
5
6
  /**
@@ -17,7 +18,39 @@ import { r2CredentialsRegistry } from "@pithy-sh/storage/src/secret/registry";
17
18
  */
18
19
  /** The name support's R2 credential bundle is stored and resolved under, per environment. */
19
20
  const SUPPORT_R2_SECRET = "support-r2-credentials";
21
+ /**
22
+ * The bucket binding it presigns — support's one optional binding, and the one an adopter declines when
23
+ * attachments are off.
24
+ *
25
+ * **Declared, because the relation was prose and a command cannot read prose (#541).** With attachments
26
+ * declined, `pithy doctor` printed `SUPPORT_BUCKET (r2) declined in pithy.config.ts` and then asked for
27
+ * `support-r2-credentials` in the same run. The binding is stated on the entry now, so the secrets side
28
+ * reads the decline the bindings side already reports.
29
+ */
30
+ const SUPPORT_BUCKET_BINDING = "SUPPORT_BUCKET";
20
31
  /** The support capability's secret-registry slice — aggregated into the shared accessor at startup. */
21
- const supportSecretsRegistry = r2CredentialsRegistry(SUPPORT_R2_SECRET);
32
+ const supportSecretsRegistry = r2CredentialsRegistry(SUPPORT_R2_SECRET, SUPPORT_BUCKET_BINDING);
33
+ /**
34
+ * **The credential this composition cannot reach**, as the `inapplicableSecrets` contribution (#541).
35
+ *
36
+ * The headline case of that issue, and the one the binding field alone does not close. Joining a
37
+ * credential to a **declined** binding only answers for a project that wrote a `declinedBindings` entry;
38
+ * a project that simply turned attachments off never wrote one, because the binding was never declared
39
+ * to decline — {@link supportNeedsBucket} suppresses it at composition. So `pithy doctor` kept asking
40
+ * for `support-r2-credentials` on exactly the configuration the issue opens with.
41
+ *
42
+ * **The predicate already existed.** Support has answered *would this configuration ever put a byte in
43
+ * `SUPPORT_BUCKET`* since #440, in one place, for the binding and the provisioner both. This is the
44
+ * third caller of the same answer rather than a fourth reading of the three settings behind it — which
45
+ * is the arrangement #440 landed precisely so that two callers could not disagree.
46
+ *
47
+ * The reason names the settings an operator would edit, all three of them, because any one of them
48
+ * turns the bucket back on and a sentence naming only `attachments.enabled` would send a reader to the
49
+ * wrong line.
50
+ */
51
+ function inapplicableAttachmentSecrets(config) {
52
+ if (supportNeedsBucket(config)) return {};
53
+ return { [SUPPORT_R2_SECRET]: "support() stores no attachments — attachments.enabled, attachments.retainRaw and submission.attachments.enabled are all off" };
54
+ }
22
55
  //#endregion
23
- export { SUPPORT_R2_SECRET, supportSecretsRegistry };
56
+ export { SUPPORT_BUCKET_BINDING, SUPPORT_R2_SECRET, inapplicableAttachmentSecrets, supportSecretsRegistry };
@@ -3,5 +3,5 @@
3
3
  /** This package's npm name — the join key against a release feed. */
4
4
  export declare const PACKAGE_NAME = "@pithy-sh/support";
5
5
  /** This package's version, stamped from its own package.json at generation time. */
6
- export declare const PACKAGE_VERSION = "0.1.5";
6
+ export declare const PACKAGE_VERSION = "0.2.0";
7
7
  //# sourceMappingURL=version.generated.d.ts.map
@@ -4,6 +4,6 @@
4
4
  /** This package's npm name — the join key against a release feed. */
5
5
  const PACKAGE_NAME = "@pithy-sh/support";
6
6
  /** This package's version, stamped from its own package.json at generation time. */
7
- const PACKAGE_VERSION = "0.1.5";
7
+ const PACKAGE_VERSION = "0.2.0";
8
8
  //#endregion
9
9
  export { PACKAGE_NAME, PACKAGE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/support",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,16 +36,16 @@
36
36
  "dependencies": {
37
37
  "@cloudflare/workers-types": "^5.20260729.1",
38
38
  "@hono/zod-validator": "^0.9.0",
39
- "@pithy-sh/core": "^0.2.0",
40
- "@pithy-sh/email": "^0.1.5",
41
- "@pithy-sh/secrets": "^0.1.5",
42
- "@pithy-sh/storage": "^0.1.5",
43
39
  "postal-mime": "^3.0.0",
44
40
  "tldts": "^7.4.10"
45
41
  },
46
42
  "peerDependencies": {
47
- "@pithy-sh/auth": "^0.1.5",
48
- "@pithy-sh/payments": "^0.2.0",
43
+ "@pithy-sh/auth": "^0.2.0",
44
+ "@pithy-sh/core": "^0.4.0",
45
+ "@pithy-sh/email": "^0.2.0",
46
+ "@pithy-sh/payments": "^0.3.0",
47
+ "@pithy-sh/secrets": "^0.2.0",
48
+ "@pithy-sh/storage": "^0.2.0",
49
49
  "hono": "^4.13.2",
50
50
  "kysely": "^0.29.0",
51
51
  "zod": "^4.4.0"
@@ -61,7 +61,11 @@
61
61
  "devDependencies": {
62
62
  "@cloudflare/vitest-plugin": "^1.0.0",
63
63
  "@pithy-sh/auth": "workspace:*",
64
+ "@pithy-sh/core": "workspace:*",
65
+ "@pithy-sh/email": "workspace:*",
64
66
  "@pithy-sh/payments": "workspace:*",
67
+ "@pithy-sh/secrets": "workspace:*",
68
+ "@pithy-sh/storage": "workspace:*",
65
69
  "@pithy-sh/tsconfig": "workspace:*",
66
70
  "@types/node": "^22.15.0",
67
71
  "@vitest/coverage-v8": "^4.1.0",
@@ -3,7 +3,7 @@
3
3
  "package": "@pithy-sh/support",
4
4
  "requiredBindings": [
5
5
  { "type": "d1", "name": "DB" },
6
- { "type": "r2", "name": "SUPPORT_BUCKET", "optional": true },
6
+ { "type": "r2", "name": "SUPPORT_BUCKET", "optional": true, "scope": "global", "resource": "support" },
7
7
  {
8
8
  "type": "workflow",
9
9
  "name": "SUPPORT_CLASSIFY",
package/src/capability.ts CHANGED
@@ -26,7 +26,7 @@ import { supportAdminRoutes } from "./http/scopes";
26
26
  import { createSupportEmailHandler } from "./inbound/handler";
27
27
  import { support_0001_threads } from "./migrations/0001_threads";
28
28
  import { resolveReplies, type SupportReplySnippets } from "./reply/snippets";
29
- import { supportSecretsRegistry } from "./secret/registry";
29
+ import { inapplicableAttachmentSecrets, SUPPORT_BUCKET_BINDING, supportSecretsRegistry } from "./secret/registry";
30
30
  import { supportExampleSeed } from "./seeds/example";
31
31
  import { PACKAGE_VERSION } from "./version.generated";
32
32
  import { supportWorkflows } from "./workflows/specs";
@@ -227,7 +227,7 @@ export function support(options: SupportOptions = {}): SupportCapability {
227
227
  // three environments for an inbox with attachments off, and `pithy doctor` call them missing
228
228
  // forever once they were deleted by hand (#440). The three settings behind `supportNeedsBucket`
229
229
  // all default `true`, so nothing changes for a project that has not turned one off.
230
- ...(supportNeedsBucket(resolved) ? [{ type: "r2" as const, name: "SUPPORT_BUCKET", optional: true }] : []),
230
+ ...(supportNeedsBucket(resolved) ? [{ type: "r2" as const, name: SUPPORT_BUCKET_BINDING, optional: true }] : []),
231
231
  // The classification Workflow the inbound handler dispatches to, derived from the spec rather
232
232
  // than listed again — one declaration, so a binding rename cannot leave the two disagreeing.
233
233
  ...workflowBindings(supportWorkflows),
@@ -241,6 +241,10 @@ export function support(options: SupportOptions = {}): SupportCapability {
241
241
  // Attachment presigning reads an R2 credential bundle through @pithy-sh/secrets.
242
242
  dependsOn: ["secrets"],
243
243
  secretRegistry: supportSecretsRegistry,
244
+ // And whether this composition can reach it at all (#541). The binding is suppressed when nothing
245
+ // would ever write to the bucket, so there is no decline for the CLI to join against — the
246
+ // capability is the only thing that knows, and it already computed the answer for the binding.
247
+ inapplicableSecrets: inapplicableAttachmentSecrets(resolved),
244
248
  workflows: supportWorkflows,
245
249
  requiredBindings,
246
250
  // Provisioning creates the routing rule that delivers mail to this Worker.
@@ -114,9 +114,13 @@ export interface SupportProvisionResult {
114
114
  * Worker whose classification host is not deployed yet, which is a window where real customer
115
115
  * messages arrive and stay `uncategorized` with nothing to say why.
116
116
  *
117
- * `environments` is the project's declaration from the root `pithy.config.ts` (#241). Every declared
118
- * environment is provisioned; an environment this skipped would be one the project deploys to with no
119
- * resources behind it — the silence the closed `ManagedEnvironment` enum used to produce.
117
+ * `environments` is **what the caller determined it can act on**, which the CLI narrows from the project's
118
+ * declaration (#241) to the environments whose app database exists (pithy-sh/pithy#512). Nothing is
119
+ * skipped here and nothing decides here: an environment reaching this list is provisioned, and one that
120
+ * did not was already reported to the operator by name, with why and with the command that fixes it.
121
+ *
122
+ * The bucket is created **whatever the list holds, including nothing** — it is one per project, and making
123
+ * it wait for the last environment to be provisioned is the ordering this change exists to undo.
120
124
  */
121
125
  export async function provisionSupport(
122
126
  provisioner: SupportProvisioner,
@@ -125,14 +129,19 @@ export async function provisionSupport(
125
129
  await provisioner.preflight();
126
130
  const bucket = await provisioner.ensureBucket();
127
131
  const search: SupportProvisionResult["search"] = [];
128
- for (const env of managedEnvironments(environments)) {
132
+ const deployed = managedEnvironments(environments);
133
+ for (const env of deployed) {
129
134
  await provisioner.deployWorker(env);
130
135
  // Per environment, because each has its own app database — and after the worker, so a database
131
136
  // that gains the index always has something able to write to it.
132
137
  search.push({ env, ...(await provisioner.ensureSearchIndex(env)) });
133
138
  }
134
- const routing = await provisioner.ensureRoutingRule();
135
- return { bucket, environments: managedEnvironments(environments), routing, search };
139
+ // Only when at least one classification host is up. The rule is what starts delivering real customer
140
+ // mail, so making it over a run that deployed nothing is the same window the ordering above closes,
141
+ // widened to the whole project.
142
+ const routing =
143
+ deployed.length > 0 ? await provisioner.ensureRoutingRule() : { created: false, skipped: true as const };
144
+ return { bucket, environments: deployed, routing, search };
136
145
  }
137
146
 
138
147
  /** The teardown seam — the inverse of {@link SupportProvisioner}. Every step idempotent. */
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { r2CredentialsRegistry } from "@pithy-sh/storage/src/secret/registry";
5
+ import { type SupportConfig, supportNeedsBucket } from "../config/config";
5
6
 
6
7
  /**
7
8
  * The secrets support reads — exactly one, and it is not support's own shape.
@@ -20,5 +21,42 @@ import { r2CredentialsRegistry } from "@pithy-sh/storage/src/secret/registry";
20
21
  /** The name support's R2 credential bundle is stored and resolved under, per environment. */
21
22
  export const SUPPORT_R2_SECRET = "support-r2-credentials";
22
23
 
24
+ /**
25
+ * The bucket binding it presigns — support's one optional binding, and the one an adopter declines when
26
+ * attachments are off.
27
+ *
28
+ * **Declared, because the relation was prose and a command cannot read prose (#541).** With attachments
29
+ * declined, `pithy doctor` printed `SUPPORT_BUCKET (r2) declined in pithy.config.ts` and then asked for
30
+ * `support-r2-credentials` in the same run. The binding is stated on the entry now, so the secrets side
31
+ * reads the decline the bindings side already reports.
32
+ */
33
+ export const SUPPORT_BUCKET_BINDING = "SUPPORT_BUCKET";
34
+
23
35
  /** The support capability's secret-registry slice — aggregated into the shared accessor at startup. */
24
- export const supportSecretsRegistry = r2CredentialsRegistry(SUPPORT_R2_SECRET);
36
+ export const supportSecretsRegistry = r2CredentialsRegistry(SUPPORT_R2_SECRET, SUPPORT_BUCKET_BINDING);
37
+
38
+ /**
39
+ * **The credential this composition cannot reach**, as the `inapplicableSecrets` contribution (#541).
40
+ *
41
+ * The headline case of that issue, and the one the binding field alone does not close. Joining a
42
+ * credential to a **declined** binding only answers for a project that wrote a `declinedBindings` entry;
43
+ * a project that simply turned attachments off never wrote one, because the binding was never declared
44
+ * to decline — {@link supportNeedsBucket} suppresses it at composition. So `pithy doctor` kept asking
45
+ * for `support-r2-credentials` on exactly the configuration the issue opens with.
46
+ *
47
+ * **The predicate already existed.** Support has answered *would this configuration ever put a byte in
48
+ * `SUPPORT_BUCKET`* since #440, in one place, for the binding and the provisioner both. This is the
49
+ * third caller of the same answer rather than a fourth reading of the three settings behind it — which
50
+ * is the arrangement #440 landed precisely so that two callers could not disagree.
51
+ *
52
+ * The reason names the settings an operator would edit, all three of them, because any one of them
53
+ * turns the bucket back on and a sentence naming only `attachments.enabled` would send a reader to the
54
+ * wrong line.
55
+ */
56
+ export function inapplicableAttachmentSecrets(config: SupportConfig): Record<string, string> {
57
+ if (supportNeedsBucket(config)) return {};
58
+ return {
59
+ [SUPPORT_R2_SECRET]:
60
+ "support() stores no attachments — attachments.enabled, attachments.retainRaw and submission.attachments.enabled are all off",
61
+ };
62
+ }
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/support";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.1.5";
16
+ export const PACKAGE_VERSION = "0.2.0";