@eide/foir-cli 0.6.3 → 0.7.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/cli.js CHANGED
@@ -2949,12 +2949,14 @@ function createOperationsMethods(client) {
2949
2949
  timeoutMs: params.timeoutMs,
2950
2950
  inputSchema: params.inputSchema,
2951
2951
  outputSchema: params.outputSchema,
2952
- streamConfig: params.streamConfig,
2953
2952
  quotas: params.quotas,
2954
2953
  retryPolicy: params.retryPolicy,
2954
+ callbackTimeoutRetryPolicy: params.callbackTimeoutRetryPolicy,
2955
2955
  allowedRoles: params.allowedRoles ?? [],
2956
2956
  precondition: params.precondition,
2957
- configId: params.configId
2957
+ configId: params.configId,
2958
+ supportsAsyncCallback: params.supportsAsyncCallback,
2959
+ callbackTtlSeconds: params.callbackTtlSeconds
2958
2960
  })
2959
2961
  );
2960
2962
  return resp.operation ?? null;
@@ -2969,11 +2971,13 @@ function createOperationsMethods(client) {
2969
2971
  timeoutMs: params.timeoutMs,
2970
2972
  inputSchema: params.inputSchema,
2971
2973
  outputSchema: params.outputSchema,
2972
- streamConfig: params.streamConfig,
2973
2974
  quotas: params.quotas,
2974
2975
  retryPolicy: params.retryPolicy,
2976
+ callbackTimeoutRetryPolicy: params.callbackTimeoutRetryPolicy,
2975
2977
  precondition: params.precondition,
2976
- isActive: params.isActive
2978
+ isActive: params.isActive,
2979
+ supportsAsyncCallback: params.supportsAsyncCallback,
2980
+ callbackTtlSeconds: params.callbackTtlSeconds
2977
2981
  })
2978
2982
  );
2979
2983
  return resp.operation ?? null;
@@ -4969,6 +4973,12 @@ async function reconcileOperations(client, configId, operations, operationBaseUr
4969
4973
  }
4970
4974
  const ex = existingByKey.get(op.key);
4971
4975
  const endpoint = resolveEndpoint(op.endpoint, operationBaseUrl);
4976
+ const supportsAsyncCallback = op.mode === "async";
4977
+ if (supportsAsyncCallback && op.timeoutMs && op.timeoutMs > 1e4) {
4978
+ console.warn(
4979
+ `\u26A0 operation "${op.key}": mode=async but timeoutMs=${op.timeoutMs} \u2014 ack should return in <10s; long timeouts mask slow extensions`
4980
+ );
4981
+ }
4972
4982
  if (ex) {
4973
4983
  const empty = {};
4974
4984
  await client.operations.updateOperation({
@@ -4979,11 +4989,13 @@ async function reconcileOperations(client, configId, operations, operationBaseUr
4979
4989
  timeoutMs: op.timeoutMs,
4980
4990
  inputSchema: op.inputSchema ?? empty,
4981
4991
  outputSchema: op.outputSchema ?? empty,
4982
- streamConfig: op.streamConfig ?? empty,
4983
4992
  quotas: op.quotas ?? empty,
4984
4993
  retryPolicy: op.retryPolicy ?? empty,
4994
+ callbackTimeoutRetryPolicy: op.callbackTimeoutRetryPolicy ?? empty,
4985
4995
  precondition: op.precondition ?? empty,
4986
- isActive: op.isActive
4996
+ isActive: op.isActive,
4997
+ supportsAsyncCallback,
4998
+ callbackTtlSeconds: op.callbackTtlSeconds
4987
4999
  });
4988
5000
  summary.operations.updated++;
4989
5001
  } else {
@@ -4997,12 +5009,14 @@ async function reconcileOperations(client, configId, operations, operationBaseUr
4997
5009
  timeoutMs: op.timeoutMs,
4998
5010
  inputSchema: op.inputSchema,
4999
5011
  outputSchema: op.outputSchema,
5000
- streamConfig: op.streamConfig,
5001
5012
  quotas: op.quotas,
5002
5013
  retryPolicy: op.retryPolicy,
5014
+ callbackTimeoutRetryPolicy: op.callbackTimeoutRetryPolicy,
5003
5015
  allowedRoles: op.allowedRoles,
5004
5016
  precondition: op.precondition,
5005
- configId
5017
+ configId,
5018
+ supportsAsyncCallback,
5019
+ callbackTtlSeconds: op.callbackTtlSeconds
5006
5020
  });
5007
5021
  summary.operations.created++;
5008
5022
  }
@@ -5571,6 +5585,24 @@ function discoverConfigFile() {
5571
5585
  }
5572
5586
  return null;
5573
5587
  }
5588
+ function syncEnvVar(envPath, key, value) {
5589
+ const envContent = existsSync4(envPath) ? readFileSync(envPath, "utf-8") : "";
5590
+ const pattern = new RegExp(`^${key}=(.*)$`, "m");
5591
+ const currentMatch = envContent.match(pattern);
5592
+ const currentValue = currentMatch?.[1];
5593
+ if (currentValue === value) {
5594
+ return "unchanged";
5595
+ }
5596
+ if (currentMatch) {
5597
+ const updated = envContent.replace(new RegExp(`^${key}=.*$`, "m"), `${key}=${value}`);
5598
+ writeFileSync2(envPath, updated, "utf-8");
5599
+ return "replaced";
5600
+ }
5601
+ const needsNewline = envContent && !envContent.endsWith("\n");
5602
+ writeFileSync2(envPath, (needsNewline ? envContent + "\n" : envContent) + `${key}=${value}
5603
+ `, "utf-8");
5604
+ return "written";
5605
+ }
5574
5606
  function writeEnvVar(envPath, key, value) {
5575
5607
  let content = "";
5576
5608
  if (existsSync4(envPath)) {
@@ -5672,31 +5704,22 @@ function registerPushCommand(program2, globalOpts) {
5672
5704
  const { secret: signingSecret } = await client.operations.getSigningSecret();
5673
5705
  if (signingSecret) {
5674
5706
  const envPath2 = resolve4(opts.env ?? ".env");
5675
- const envContent = existsSync4(envPath2) ? readFileSync(envPath2, "utf-8") : "";
5676
- const currentMatch = envContent.match(/^FOIR_WEBHOOK_SECRET=(.*)$/m);
5677
- const currentValue = currentMatch?.[1];
5678
- if (currentValue !== signingSecret) {
5679
- if (currentMatch) {
5680
- const updated = envContent.replace(
5681
- /^FOIR_WEBHOOK_SECRET=.*$/m,
5682
- `FOIR_WEBHOOK_SECRET=${signingSecret}`
5683
- );
5684
- writeFileSync2(envPath2, updated, "utf-8");
5707
+ const syncAndLog = (key, label) => {
5708
+ const result = syncEnvVar(envPath2, key, signingSecret);
5709
+ if (result === "unchanged") {
5710
+ console.log(chalk6.dim(` ${label}: ${key} already up to date, skipped`));
5711
+ } else if (result === "replaced") {
5685
5712
  console.log(
5686
- chalk6.yellow("\u27F3 Webhook signing secret") + chalk6.dim(` \u2192 FOIR_WEBHOOK_SECRET updated in ${envPath2}`)
5713
+ chalk6.yellow(`\u27F3 ${label}`) + chalk6.dim(` \u2192 ${key} updated in ${envPath2}`)
5687
5714
  );
5688
5715
  } else {
5689
- envWrites.push({
5690
- key: "FOIR_WEBHOOK_SECRET",
5691
- value: signingSecret,
5692
- label: "Webhook signing secret"
5693
- });
5716
+ console.log(
5717
+ chalk6.green(`\u2713 ${label}`) + chalk6.dim(` \u2192 ${key} written to ${envPath2}`)
5718
+ );
5694
5719
  }
5695
- } else {
5696
- console.log(
5697
- chalk6.dim(" Webhook signing secret: FOIR_WEBHOOK_SECRET already up to date, skipped")
5698
- );
5699
- }
5720
+ };
5721
+ syncAndLog("FOIR_WEBHOOK_SECRET", "Webhook signing secret");
5722
+ syncAndLog("FOIR_SIGNING_SECRET", "Callback signing secret");
5700
5723
  }
5701
5724
  if (envWrites.length > 0) {
5702
5725
  console.log();
@@ -90,8 +90,6 @@ interface ApplyConfigOperationInput {
90
90
  timeoutMs?: number;
91
91
  inputSchema?: Record<string, unknown>;
92
92
  outputSchema?: Record<string, unknown>;
93
- /** Streaming configuration for SSE/chunked responses. */
94
- streamConfig?: Record<string, unknown>;
95
93
  /** Usage quota rules (rate limits per customer/user/tenant). */
96
94
  quotas?: {
97
95
  rules: QuotaRule[];
@@ -102,6 +100,45 @@ interface ApplyConfigOperationInput {
102
100
  allowedRoles?: string[];
103
101
  /** Precondition that must be met before execution (e.g., segment membership). */
104
102
  precondition?: Precondition;
103
+ /**
104
+ * Execution mode the operation supports. `sync` blocks the platform worker
105
+ * on the HTTP call (≤30s realistically). `async` dispatches with
106
+ * 202-ack semantics — the extension returns 202 immediately and calls
107
+ * back via the typed GraphQL mutation when work completes. Use `async`
108
+ * for long-running work that exceeds the 30s wall clock.
109
+ *
110
+ * There is only one flavour of async on the platform: the callback-based
111
+ * handshake. The prior "fire-and-forget" meaning of async was retired
112
+ * in 2026-04 — every ASYNC dispatch now carries a scoped callback token
113
+ * and the platform tracks the execution to terminal state.
114
+ *
115
+ * Default: `sync`. The CLI maps this to `supportsAsyncCallback` on the
116
+ * platform Operation row — the operation can still be dispatched in SYNC
117
+ * mode at runtime when callers want a blocking call.
118
+ */
119
+ mode?: 'sync' | 'async';
120
+ /**
121
+ * How long the platform waits for a callback before marking the execution
122
+ * `timed_out` and (per retry policy) re-queueing or sending to DLQ. Falls
123
+ * back to project setting `operations.callback_default_ttl`, then 24h.
124
+ * Clamped to [5m, 7d]. Only meaningful when `mode === 'async'`.
125
+ */
126
+ callbackTtlSeconds?: number;
127
+ /**
128
+ * Retry policy applied specifically when the callback times out. Distinct
129
+ * from `retryPolicy`, which governs HTTP dispatch failures (network, 5xx).
130
+ * Splitting these lets operators retry dispatch failures aggressively while
131
+ * being conservative about callback timeouts — re-running an hour-long job
132
+ * that timed out because the upstream was slow tends to burn another hour
133
+ * and hit the same slowness.
134
+ *
135
+ * Shape mirrors `retryPolicy` (`{ maxRetries }`). Falls back to
136
+ * `retryPolicy` when unset, then to the watchdog default (3).
137
+ * Set `{ maxRetries: 0 }` to opt out of retry on timeout entirely.
138
+ */
139
+ callbackTimeoutRetryPolicy?: {
140
+ maxRetries?: number;
141
+ };
105
142
  }
106
143
  interface ApplyConfigSegmentInput {
107
144
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -50,7 +50,7 @@
50
50
  "@bufbuild/protovalidate": "^1.1.1",
51
51
  "@connectrpc/connect": "^2.0.0",
52
52
  "@connectrpc/connect-node": "^2.0.0",
53
- "@eide/foir-proto-ts": "^0.11.0",
53
+ "@eide/foir-proto-ts": "^0.16.0",
54
54
  "chalk": "^5.3.0",
55
55
  "commander": "^12.1.0",
56
56
  "dotenv": "^16.4.5",