@vxil/config 0.5.1 → 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/index.d.ts CHANGED
@@ -106,18 +106,45 @@ export interface CollectionDef {
106
106
  * session.revoked — auth.session.revoked { user_id, session_id, reason }
107
107
  * signin.failure — auth.signin.failure { email_hash | user_id, reason } */
108
108
  export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
109
+ /** The opt-in re-delivery of a platform-delivered trigger (F33, 2026-09-25).
110
+ * DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
111
+ * the handler returned — an application error is observed (`functions.run.failed`),
112
+ * never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
113
+ * `authHook` trigger, a non-2xx answer of the handler is handed back to the
114
+ * jobs retry ladder instead (60 s, 120 s, … backoff) and the last failed
115
+ * attempt dead-letters the run (`job.dead_lettered`, replayable from the
116
+ * Jobs page). Each attempt is a billed invocation and carries the SAME
117
+ * `idempotency_key` — dedupe on it. Effective attempts =
118
+ * min(maxAttempts, the project's `jobs.retry.defaultMaxAttempts`, default 5).
119
+ * A handler that partially writes and then fails re-fires its downstream
120
+ * events on EVERY attempt, so the chain is a tree (up to maxAttempts children
121
+ * per hop): the causal-depth guard bounds its length (32 hops), not its size —
122
+ * the breaker, the loop guard, the dead-letter quota and the daily share do.
123
+ * Write only once you will return 2xx, or dedupe on idempotency_key. Not accepted on `http` (it returns its
124
+ * real status to its caller) or `cron` (a retry would overlap the next tick). */
125
+ export interface FunctionTriggerRetry {
126
+ /** 1–5 attempts in all (1 = a failed attempt dead-letters at once, no re-delivery) */
127
+ maxAttempts: number;
128
+ }
109
129
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
110
130
  * authHook fires on ONE auth lifecycle event (default `user.created` — the
111
131
  * post-signup hook; at-least-once, ~1min fanout latency). */
112
132
  export type FunctionTrigger = {
113
133
  kind: 'http';
114
134
  path?: string;
115
- } | {
135
+ }
136
+ /** `overlap: 'skip'` (2026-10-01): a due tick fires nothing while the
137
+ * previous tick's run is still open (queued / running / retrying / waiting /
138
+ * delayed) — the slot is counted, never caught up. Default `'allow'`. Every
139
+ * cron envelope carries `scheduled_for`, the slot it was due for. */
140
+ | {
116
141
  kind: 'cron';
117
142
  schedule: string;
143
+ overlap?: 'allow' | 'skip';
118
144
  } | {
119
145
  kind: 'queue';
120
146
  source: string;
147
+ retry?: FunctionTriggerRetry;
121
148
  }
122
149
  /** `webhook`: the function is woken by a webhook_subscription fan-out whose
123
150
  * target_url is `<edge>/v1/internal/fn/trigger/<name>?tenant=<id>`. That
@@ -132,6 +159,7 @@ export type FunctionTrigger = {
132
159
  | {
133
160
  kind: 'webhook';
134
161
  source: string;
162
+ retry?: FunctionTriggerRetry;
135
163
  }
136
164
  /** `cmsHook`: fires after a CMS write in `collection`. The platform filters
137
165
  * deliveries on the binding (2026-09-19): `beforeCreate` → created only,
@@ -142,15 +170,26 @@ export type FunctionTrigger = {
142
170
  kind: 'cmsHook';
143
171
  collection: string;
144
172
  event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite';
173
+ retry?: FunctionTriggerRetry;
145
174
  } | {
146
175
  kind: 'authHook';
147
176
  event?: AuthHookEvent;
177
+ retry?: FunctionTriggerRetry;
148
178
  };
149
179
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
150
180
  export interface FunctionDef {
151
181
  /** path to the ES-module entrypoint, e.g. ./functions/<name>.ts */
152
182
  entry: string;
153
183
  trigger?: FunctionTrigger;
184
+ /** Further bindings beside `trigger` (2026-09-25): a function is code + a SET
185
+ * of trigger bindings, so one bundle can be reached by several front doors —
186
+ * a webhook AND a cron, an http door AND a cmsHook. `trigger` and `triggers`
187
+ * may both be present; the deploy carries them in that order as
188
+ * `bindings[]`, at most 8 in all (the CLI refuses a 9th before the wire).
189
+ * Every binding lands the same isolate; only the envelope's `trigger` /
190
+ * `payload` differ. Each platform-delivered binding is billed per
191
+ * invocation and bounded by the loop guard and the breaker. */
192
+ triggers?: FunctionTrigger[];
154
193
  /** clamped by DENY_FUNCTION_SCOPES at deploy (admin, wildcard, features:write,
155
194
  * functions:write, secrets:write rejected; payments:write / notifications:send /
156
195
  * users:* allowed as owner-granted business scopes). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/config",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Typed vxil.config.ts authoring for the vxil backend platform — defineConfig, feature and cms field definitions (published for @vxil/cli).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vxil.com",
@@ -22,7 +22,7 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
- "@vxil/feature-configs": "0.5.1"
25
+ "@vxil/feature-configs": "0.7.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // types); the control plane never executes this file. This keeps config-as-code
7
7
  // §7.3-consistent — config is DATA; the only tenant CODE that crosses into vxil
8
8
  // is (a) the Lane-A hook expressions (a closed sandbox) and (b) the functions/
9
- // sources (the paid, opt-in, egress-guarded crossing). Everything here is
9
+ // sources (the opt-in, plan-limited, egress-guarded crossing). Everything here is
10
10
  // declarative. See https://vxil.com/docs/guide/05-typed-sdk-and-cli.
11
11
  import type {
12
12
  NotificationsConfig, JobsConfig, AuthConfig, RateLimitsConfig, FilesConfig,
@@ -130,13 +130,38 @@ export interface CollectionDef {
130
130
  * signin.failure — auth.signin.failure { email_hash | user_id, reason } */
131
131
  export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
132
132
 
133
+ /** The opt-in re-delivery of a platform-delivered trigger (F33, 2026-09-25).
134
+ * DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
135
+ * the handler returned — an application error is observed (`functions.run.failed`),
136
+ * never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
137
+ * `authHook` trigger, a non-2xx answer of the handler is handed back to the
138
+ * jobs retry ladder instead (60 s, 120 s, … backoff) and the last failed
139
+ * attempt dead-letters the run (`job.dead_lettered`, replayable from the
140
+ * Jobs page). Each attempt is a billed invocation and carries the SAME
141
+ * `idempotency_key` — dedupe on it. Effective attempts =
142
+ * min(maxAttempts, the project's `jobs.retry.defaultMaxAttempts`, default 5).
143
+ * A handler that partially writes and then fails re-fires its downstream
144
+ * events on EVERY attempt, so the chain is a tree (up to maxAttempts children
145
+ * per hop): the causal-depth guard bounds its length (32 hops), not its size —
146
+ * the breaker, the loop guard, the dead-letter quota and the daily share do.
147
+ * Write only once you will return 2xx, or dedupe on idempotency_key. Not accepted on `http` (it returns its
148
+ * real status to its caller) or `cron` (a retry would overlap the next tick). */
149
+ export interface FunctionTriggerRetry {
150
+ /** 1–5 attempts in all (1 = a failed attempt dead-letters at once, no re-delivery) */
151
+ maxAttempts: number;
152
+ }
153
+
133
154
  /** A function trigger (the §7.3 crossing). cmsHook fires on a CMS write;
134
155
  * authHook fires on ONE auth lifecycle event (default `user.created` — the
135
156
  * post-signup hook; at-least-once, ~1min fanout latency). */
136
157
  export type FunctionTrigger =
137
158
  | { kind: 'http'; path?: string }
138
- | { kind: 'cron'; schedule: string }
139
- | { kind: 'queue'; source: string }
159
+ /** `overlap: 'skip'` (2026-10-01): a due tick fires nothing while the
160
+ * previous tick's run is still open (queued / running / retrying / waiting /
161
+ * delayed) — the slot is counted, never caught up. Default `'allow'`. Every
162
+ * cron envelope carries `scheduled_for`, the slot it was due for. */
163
+ | { kind: 'cron'; schedule: string; overlap?: 'allow' | 'skip' }
164
+ | { kind: 'queue'; source: string; retry?: FunctionTriggerRetry }
140
165
  /** `webhook`: the function is woken by a webhook_subscription fan-out whose
141
166
  * target_url is `<edge>/v1/internal/fn/trigger/<name>?tenant=<id>`. That
142
167
  * subscription is AUTO-WIRED on deploy (2026-09-19): the platform reconciles
@@ -147,20 +172,29 @@ export type FunctionTrigger =
147
172
  * also enforced at delivery: a non-matching event is ACK-200 skipped.
148
173
  * Declaring the binding is what opts the function into event delivery; a
149
174
  * receiver that declares only `cron` keeps the older cron+{} wake. */
150
- | { kind: 'webhook'; source: string }
175
+ | { kind: 'webhook'; source: string; retry?: FunctionTriggerRetry }
151
176
  /** `cmsHook`: fires after a CMS write in `collection`. The platform filters
152
177
  * deliveries on the binding (2026-09-19): `beforeCreate` → created only,
153
178
  * `beforeUpdate` → updated only, `beforeWrite` → created|updated; a write to
154
179
  * another collection never wakes the function. Deleted/bulk events are not
155
180
  * delivered to a cmsHook binding. */
156
- | { kind: 'cmsHook'; collection: string; event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite' }
157
- | { kind: 'authHook'; event?: AuthHookEvent };
181
+ | { kind: 'cmsHook'; collection: string; event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite'; retry?: FunctionTriggerRetry }
182
+ | { kind: 'authHook'; event?: AuthHookEvent; retry?: FunctionTriggerRetry };
158
183
 
159
184
  /** A deployed tenant function — source in functions/, deployed on `vxil push`. */
160
185
  export interface FunctionDef {
161
186
  /** path to the ES-module entrypoint, e.g. ./functions/<name>.ts */
162
187
  entry: string;
163
188
  trigger?: FunctionTrigger;
189
+ /** Further bindings beside `trigger` (2026-09-25): a function is code + a SET
190
+ * of trigger bindings, so one bundle can be reached by several front doors —
191
+ * a webhook AND a cron, an http door AND a cmsHook. `trigger` and `triggers`
192
+ * may both be present; the deploy carries them in that order as
193
+ * `bindings[]`, at most 8 in all (the CLI refuses a 9th before the wire).
194
+ * Every binding lands the same isolate; only the envelope's `trigger` /
195
+ * `payload` differ. Each platform-delivered binding is billed per
196
+ * invocation and bounded by the loop guard and the breaker. */
197
+ triggers?: FunctionTrigger[];
164
198
  /** clamped by DENY_FUNCTION_SCOPES at deploy (admin, wildcard, features:write,
165
199
  * functions:write, secrets:write rejected; payments:write / notifications:send /
166
200
  * users:* allowed as owner-granted business scopes). */