@vxil/cli 0.18.0 → 0.19.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.
|
@@ -10,12 +10,16 @@ export declare const HOOK_LIMITS: {
|
|
|
10
10
|
readonly maxReadHooksPerCollection: 10;
|
|
11
11
|
};
|
|
12
12
|
/** The ONLY root variables an expression may reference. `caller` is the VERIFIED
|
|
13
|
-
* end-user principal (https://vxil.com/docs/guide/09-security-and-multitenancy) — read-only
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* end-user principal (https://vxil.com/docs/guide/09-security-and-multitenancy) — read-only.
|
|
14
|
+
* It carries `caller.endUserId` (the verified session sub, or null),
|
|
15
|
+
* `caller.principal` ('end_user' | 'tenant') and `caller.roles` (the VERIFIED
|
|
16
|
+
* session role claims of the signed-in session — never a body or header
|
|
17
|
+
* value; null in server-caller mode). READ hooks always get it
|
|
18
|
+
* (server mode: `{ endUserId: null, principal: 'tenant', roles: null }`).
|
|
19
|
+
* WRITE hooks get it in verified end-user mode only; on a
|
|
20
|
+
* server write it is null, exactly like `before` on a create — so a role rule
|
|
21
|
+
* reads `isNull(caller) || contains(caller.roles, 'manager')`. `contains` on
|
|
22
|
+
* an array is exact element membership. */
|
|
19
23
|
export declare const HOOK_ROOT_VARS: readonly ["item", "before", "now", "caller"];
|
|
20
24
|
/** The ONLY callable functions. Each is pure + deterministic + bounded. */
|
|
21
25
|
export declare const HOOK_FUNCTIONS: readonly ["min", "max", "abs", "round", "floor", "ceil", "sqrt", "pow", "sign", "len", "lower", "upper", "trim", "substr", "contains", "startsWith", "endsWith", "concat", "coalesce", "ifNull", "not", "isNull", "number", "string", "bool", "daysBetween", "yearsBetween"];
|
|
@@ -80,20 +84,25 @@ export declare function parseExpr(src: string): Node;
|
|
|
80
84
|
/** Walk the AST and reject anything outside the allow-list, plus depth bounds.
|
|
81
85
|
* Returns an array of human-readable errors (empty = safe). Pure, no eval. */
|
|
82
86
|
export declare function validateAst(root: Node): string[];
|
|
83
|
-
/** The VERIFIED caller/session context for
|
|
84
|
-
*
|
|
85
|
-
*
|
|
87
|
+
/** The VERIFIED caller/session context for Lane-A hooks (guide ch. 7).
|
|
88
|
+
* Read-only. Read hooks always get it; write hooks get it in verified end-user
|
|
89
|
+
* mode only (a server write passes null — see HOOK_ROOT_VARS). */
|
|
86
90
|
export interface HookCaller {
|
|
87
91
|
/** the verified end-user session sub, or null in server-caller mode. */
|
|
88
92
|
endUserId: string | null;
|
|
89
93
|
/** 'end_user' when a session was verified at the edge, else 'tenant'. */
|
|
90
94
|
principal: 'end_user' | 'tenant';
|
|
95
|
+
/** the VERIFIED session role claims of the signed-in session; `[]` for a
|
|
96
|
+
* session without roles, null in server-caller mode. Optional so an older
|
|
97
|
+
* caller object still reads
|
|
98
|
+
* as "no roles". */
|
|
99
|
+
roles?: readonly string[] | null;
|
|
91
100
|
}
|
|
92
101
|
export interface HookContext {
|
|
93
102
|
item: Record<string, unknown>;
|
|
94
103
|
before: Record<string, unknown> | null;
|
|
95
104
|
now: string;
|
|
96
|
-
/** verified caller/session context
|
|
105
|
+
/** verified caller/session context; null on a server write. */
|
|
97
106
|
caller?: HookCaller | null;
|
|
98
107
|
}
|
|
99
108
|
/** A mutable step counter SHARED across many evalExpr calls (the read path's
|
|
@@ -127,7 +136,10 @@ export interface HookRunResult {
|
|
|
127
136
|
* Mutates a CLONE: derive hooks set fields; a validate hook that returns falsy
|
|
128
137
|
* throws HookRejection (→ the caller maps to a 422 and the tx rolls back).
|
|
129
138
|
* Throws HookEvalError on a runtime fault (also a clean 422, never a 500). */
|
|
130
|
-
export declare function runWriteHooks(hooks: Record<string, HookDef> | undefined, collection: string, phase: 'create' | 'update', data: Record<string, unknown>, before: Record<string, unknown> | null, now: string
|
|
139
|
+
export declare function runWriteHooks(hooks: Record<string, HookDef> | undefined, collection: string, phase: 'create' | 'update', data: Record<string, unknown>, before: Record<string, unknown> | null, now: string,
|
|
140
|
+
/** the VERIFIED end-user caller (endUserId + principal + roles) in
|
|
141
|
+
* end-user mode; omitted / null on a server write (expressions see null). */
|
|
142
|
+
caller?: HookCaller | null): HookRunResult;
|
|
131
143
|
export interface ReadHookResult {
|
|
132
144
|
/** Surviving rows in input order. Each is a SHALLOW COPY with a copied `data`
|
|
133
145
|
* bag — the caller's rows are never mutated and nothing here is persisted. */
|
|
@@ -154,8 +166,8 @@ export interface ReadHookResult {
|
|
|
154
166
|
* fire here, and read events never fire in runWriteHooks. */
|
|
155
167
|
export declare function runReadHooks(hooks: Record<string, HookDef> | undefined, collection: string, rows: ReadonlyArray<Record<string, unknown>>, now: string,
|
|
156
168
|
/** verified caller/session context (guide ch. 7) — read-only, exposed to
|
|
157
|
-
* expressions as `caller.endUserId` / `caller.principal
|
|
158
|
-
* (server-caller mode). */
|
|
169
|
+
* expressions as `caller.endUserId` / `caller.principal` / `caller.roles`.
|
|
170
|
+
* Omitted ⇒ null (server-caller mode). */
|
|
159
171
|
caller?: HookCaller | null): ReadHookResult;
|
|
160
172
|
export interface ExprRef {
|
|
161
173
|
root: 'item' | 'before' | 'now' | 'caller';
|
|
@@ -35,4 +35,22 @@ interface CdcRuleLike {
|
|
|
35
35
|
}
|
|
36
36
|
/** Cross-field rule for CmsConfig.cdc (called from validateFeatureConfig). */
|
|
37
37
|
export declare function validateCdcConfig(cdc: Record<string, CdcRuleLike> | undefined): string[];
|
|
38
|
+
/** What a payload:'full' change-data rule must know about its target
|
|
39
|
+
* collection's read gates. `readRoleFields`: fields with a non-empty
|
|
40
|
+
* `readRoles`; `endUserAccess`: the collection's end-user access mode. */
|
|
41
|
+
export interface CdcCollectionGates {
|
|
42
|
+
readRoleFields?: readonly string[];
|
|
43
|
+
endUserAccess?: 'readwrite' | 'read' | 'none';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A `payload: 'full'` change-data rule inlines the stored row into a realtime
|
|
47
|
+
* frame that every subscriber of the channel receives — it does not go through
|
|
48
|
+
* the read projection. So it must never target a collection whose reads are
|
|
49
|
+
* gated: a field with `readRoles` (the frame would carry the gated value), or
|
|
50
|
+
* an `endUserAccess` of 'read' / 'none' (the frame would bypass the
|
|
51
|
+
* collection's end-user gate). Pure; one problem line per offending ENABLED
|
|
52
|
+
* rule (an unknown collection is skipped). The safe shape is `payload: 'ids'`
|
|
53
|
+
* and a re-read through the cms, where every gate applies.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cdcFullFramesOverGatedCollections(cdc: unknown, collections: Record<string, CdcCollectionGates>): string[];
|
|
38
56
|
export {};
|
|
@@ -218,6 +218,7 @@ declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
|
|
|
218
218
|
email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
219
219
|
name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
220
220
|
roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
221
|
+
rolePrefix: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
221
222
|
}>>;
|
|
222
223
|
allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
223
224
|
autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
@@ -275,6 +276,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
275
276
|
email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
276
277
|
name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
277
278
|
roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
279
|
+
rolePrefix: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
278
280
|
}>>;
|
|
279
281
|
allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
280
282
|
autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
@@ -319,6 +321,8 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
319
321
|
breachedPasswords: import("@sinclair/typebox").TBoolean;
|
|
320
322
|
captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
321
323
|
allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
324
|
+
inviteOnly: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
325
|
+
allowedEmailDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
322
326
|
}>>;
|
|
323
327
|
}>;
|
|
324
328
|
/** The security bag as persisted (present ⇒ leaf defaults applied). */
|
|
@@ -633,6 +637,7 @@ export declare const RagConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
633
637
|
topK: import("@sinclair/typebox").TInteger;
|
|
634
638
|
mode: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"hybrid">, import("@sinclair/typebox").TLiteral<"vector">, import("@sinclair/typebox").TLiteral<"keyword">]>;
|
|
635
639
|
minScore: import("@sinclair/typebox").TNumber;
|
|
640
|
+
minSimilarity: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
636
641
|
rerank: import("@sinclair/typebox").TBoolean;
|
|
637
642
|
}>;
|
|
638
643
|
boosts: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
package/dist/config.d.ts
CHANGED
|
@@ -121,6 +121,17 @@ export interface CollectionDef {
|
|
|
121
121
|
* `vxil push --allow-destructive`. Any other value fails `vxil plan` /
|
|
122
122
|
* `vxil push` (it never falls back to 'readwrite'). */
|
|
123
123
|
endUserAccess?: 'readwrite' | 'read' | 'none';
|
|
124
|
+
/** Role slugs one of which a VERIFIED end user must hold to WRITE this
|
|
125
|
+
* collection (create, update, $inc, delete, publish, transaction step,
|
|
126
|
+
* filtered delete, batch write, and an end-user delete's cascade into it) —
|
|
127
|
+
* otherwise `403 role_required`. Reads are unaffected. Roles are the
|
|
128
|
+
* session's verified role claims (its orgs roles — the
|
|
129
|
+
* same ones `readRoles` and `caller.roles` in hooks see). Server keys are
|
|
130
|
+
* never affected. Absent / `[]` = ungated. Up to 16 slugs
|
|
131
|
+
* (`^[a-z0-9][a-z0-9_-]{0,31}$`). Stored on the collection (model data, not
|
|
132
|
+
* a config leaf) and carried by BOTH push paths; widening (clearing, or
|
|
133
|
+
* adding a role) needs `vxil push --allow-destructive`. */
|
|
134
|
+
writeRoles?: string[];
|
|
124
135
|
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
125
136
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
126
137
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
package/dist/vxil.js
CHANGED
|
@@ -3705,7 +3705,16 @@ var OidcProviderSchema = Type.Object({
|
|
|
3705
3705
|
claims: Type.Optional(Type.Object({
|
|
3706
3706
|
email: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
3707
3707
|
name: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
3708
|
-
roles: Type.Optional(Type.String({ minLength: 1, maxLength: 64 }))
|
|
3708
|
+
roles: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
|
|
3709
|
+
// G-38b: an optional namespace prepended to every mapped IdP role (e.g.
|
|
3710
|
+
// `idp-`), so an IdP group named like an app role (`finance`, `owner`)
|
|
3711
|
+
// arrives as `idp-finance` and cannot pass a `readRoles: ['finance']`
|
|
3712
|
+
// gate. Absent = the raw group names (the shipped behaviour). The prefixed
|
|
3713
|
+
// role still obeys the 32-char role bound (longer ones are dropped).
|
|
3714
|
+
// The alphabet is the cms readRoles / orgs role slug alphabet
|
|
3715
|
+
// (`^[a-z0-9][a-z0-9_-]*$`, cms-v1 READ_ROLE_RE), so a prefixed role can
|
|
3716
|
+
// itself be named in a gate — a `:`/`.`/uppercase prefix never could.
|
|
3717
|
+
rolePrefix: Type.Optional(Type.String({ minLength: 1, maxLength: 16, pattern: "^[a-z0-9][a-z0-9_-]{0,15}$" }))
|
|
3709
3718
|
})),
|
|
3710
3719
|
// when non-empty, the asserted email's domain MUST be listed (fail-closed:
|
|
3711
3720
|
// no email ⇒ refused) → 403 oidc_domain_not_allowed; a listed domain also
|
|
@@ -3895,6 +3904,26 @@ var AuthConfigSchema = Type.Object({
|
|
|
3895
3904
|
allowedRedirectOrigins: Type.Optional(Type.Array(
|
|
3896
3905
|
Type.String({ minLength: 8, maxLength: 253, pattern: REDIRECT_ORIGIN_PATTERN }),
|
|
3897
3906
|
{ maxItems: 32 }
|
|
3907
|
+
)),
|
|
3908
|
+
// G-3 sign-up gating (2026-10-05). Both Optional with NO default, so an
|
|
3909
|
+
// existing manifest folds byte-identically and the gate is opt-in. When
|
|
3910
|
+
// EITHER is set, every self-service account-create path (password sign-up,
|
|
3911
|
+
// magic-link / OTP pre-register, the guest → email claim, social sign-in
|
|
3912
|
+
// with google/github/apple/facebook) is checked, fail-closed:
|
|
3913
|
+
// • inviteOnly — a new account only for an address the tenant already
|
|
3914
|
+
// put in its user registry server-side (POST /v1/users, an import);
|
|
3915
|
+
// • allowedEmailDomains — otherwise, only for an address at one of these
|
|
3916
|
+
// domains (exact, lowercase; a provider must assert the address
|
|
3917
|
+
// VERIFIED).
|
|
3918
|
+
// With either set, password sign-up is refused (it proves nothing about
|
|
3919
|
+
// the address — a stranger could squat an invitee's address), guest
|
|
3920
|
+
// sign-in is refused (a guest is nobody's invitee), and an existing
|
|
3921
|
+
// account is never affected. The generic OIDC issuer keeps its own gate
|
|
3922
|
+
// (providers.oidc.allowedDomains + IdP assignment). auth-v1 signupGate.ts.
|
|
3923
|
+
inviteOnly: Type.Optional(Type.Boolean()),
|
|
3924
|
+
allowedEmailDomains: Type.Optional(Type.Array(
|
|
3925
|
+
Type.String({ minLength: 3, maxLength: 253, pattern: "^[a-z0-9][a-z0-9.-]*\\.[a-z]{2,}$" }),
|
|
3926
|
+
{ maxItems: 32 }
|
|
3898
3927
|
))
|
|
3899
3928
|
}))
|
|
3900
3929
|
});
|
|
@@ -4494,6 +4523,12 @@ var RagConfigSchema = Type.Object({
|
|
|
4494
4523
|
),
|
|
4495
4524
|
// drop retrieved chunks below this score BEFORE budgeting (0 = keep all).
|
|
4496
4525
|
minScore: Type.Number({ default: 0 }),
|
|
4526
|
+
// drop retrieved chunks whose cosine `similarity` to the query is
|
|
4527
|
+
// below this floor (-1..1). Unlike minScore (a floor on the RRF rank
|
|
4528
|
+
// value, ~0.016-0.033 at the top) this is the relevance threshold a RAG
|
|
4529
|
+
// app means. Absent = no floor; a hit with no measured similarity (no
|
|
4530
|
+
// query vector: keyword mode) is never dropped by it.
|
|
4531
|
+
minSimilarity: Type.Optional(Type.Number({ minimum: -1, maximum: 1 })),
|
|
4497
4532
|
// forward rerank:true to vector-search's post-RRF BYO rerank pass (it owns
|
|
4498
4533
|
// the provider + key); inert until that pass exists/is configured there.
|
|
4499
4534
|
rerank: Type.Boolean({ default: false })
|
|
@@ -5030,9 +5065,9 @@ function lowerAllTriggerBindings(def, name) {
|
|
|
5030
5065
|
return bindings;
|
|
5031
5066
|
}
|
|
5032
5067
|
var CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
|
|
5033
|
-
var VXIL_CONFIG_PKG_VERSION = "0.
|
|
5034
|
-
var VXIL_SDK_PKG_VERSION = "0.
|
|
5035
|
-
var VXIL_CLI_PKG_VERSION = "0.
|
|
5068
|
+
var VXIL_CONFIG_PKG_VERSION = "0.13.0";
|
|
5069
|
+
var VXIL_SDK_PKG_VERSION = "0.19.0";
|
|
5070
|
+
var VXIL_CLI_PKG_VERSION = "0.19.0";
|
|
5036
5071
|
function ensureScaffoldPackageJson(cwd, opts = {}) {
|
|
5037
5072
|
const file = resolve(cwd, "package.json");
|
|
5038
5073
|
const wanted = {
|
|
@@ -7617,6 +7652,15 @@ var KNOWN_SCOPES = [
|
|
|
7617
7652
|
// so pre-existing keys keep working; mint this when a key should see usage
|
|
7618
7653
|
// and nothing else.
|
|
7619
7654
|
"usage:read",
|
|
7655
|
+
// Narrow READ-ONLY scope for the audit stream (G-15, 2026-10-05): GET
|
|
7656
|
+
// /v1/audit and GET /v1/audit/export accept it alongside 'features:read'
|
|
7657
|
+
// (any-of), so an ops/admin function or a dedicated key reads the audit log
|
|
7658
|
+
// WITHOUT holding features:read — which is also the door of
|
|
7659
|
+
// GET /v1/export/:feature (a whole-project export credential). Both routes
|
|
7660
|
+
// stay server-only; a public / thin-client key can never carry it
|
|
7661
|
+
// (PUBLIC_KEY_DENY_SCOPES), and a function receives it on its confined
|
|
7662
|
+
// control-plane callback token (CONTROL_PLANE_FUNCTION_SCOPES).
|
|
7663
|
+
"audit:read",
|
|
7620
7664
|
"admin"
|
|
7621
7665
|
];
|
|
7622
7666
|
var FUNCTION_SCOPE_AUDIENCE = {
|
|
@@ -7645,7 +7689,14 @@ var FUNCTION_SCOPE_AUDIENCE = {
|
|
|
7645
7689
|
// CONTROL_PLANE_FUNCTION_SCOPES (scopesForFunctionAudience) so the same token
|
|
7646
7690
|
// can never satisfy any other scope-gated control-plane route.
|
|
7647
7691
|
users: "control-plane",
|
|
7648
|
-
usage: "control-plane"
|
|
7692
|
+
usage: "control-plane",
|
|
7693
|
+
// G-15: the audit stream (/v1/audit, /v1/audit/export) is a control-plane
|
|
7694
|
+
// route too; `audit:read` joins the confined claim below
|
|
7695
|
+
audit: "control-plane",
|
|
7696
|
+
// webhooks-in (the inbound receiver's READ routes); the token is confined to
|
|
7697
|
+
// WEBHOOKS_FUNCTION_SCOPES, and webhooks:write stays in SCOPES_NOT_FOR_FUNCTIONS
|
|
7698
|
+
// (which wins over the prefix — see functionScopeAudience).
|
|
7699
|
+
webhooks: "webhooks"
|
|
7649
7700
|
};
|
|
7650
7701
|
var FUNCTION_SCOPE_PREFIX_ALIASES = {
|
|
7651
7702
|
"rate-limits": "ratelimits",
|
|
@@ -7653,7 +7704,20 @@ var FUNCTION_SCOPE_PREFIX_ALIASES = {
|
|
|
7653
7704
|
vector: "vector-search",
|
|
7654
7705
|
feeds: "activity-feed"
|
|
7655
7706
|
};
|
|
7656
|
-
var
|
|
7707
|
+
var SCOPES_NOT_FOR_FUNCTIONS = {
|
|
7708
|
+
admin: "never grantable to a function (DENY_FUNCTION_SCOPES strips it at deploy)",
|
|
7709
|
+
"secrets:write": "never grantable to a function (self-integrity: DENY_FUNCTION_SCOPES strips it at deploy)",
|
|
7710
|
+
"features:write": "never grantable to a function (self-integrity: DENY_FUNCTION_SCOPES strips it at deploy)",
|
|
7711
|
+
"functions:write": "never grantable to a function (self-integrity: DENY_FUNCTION_SCOPES strips it at deploy)",
|
|
7712
|
+
"features:read": "GET /v1/features is privilege-independent (any authenticated principal) and no other route needs it from a function \u2014 the control-plane callback token is confined to users:*/usage:read",
|
|
7713
|
+
"functions:invoke": "there is no function\u2192function lane by design (no aud=functions token is ever minted) \u2014 chain work through a cms/webhook/queue trigger instead",
|
|
7714
|
+
"functions:read": "GET /v1/functions is privilege-independent and the manifest is deploy-time data \u2014 read your own config from vxil.config.ts, not at invoke",
|
|
7715
|
+
"webhooks:write": "webhook subscriptions are deploy-managed trigger wiring (auto-wired from bindings) \u2014 a function must not rewrite its own triggers (self-integrity, same class as functions:write)",
|
|
7716
|
+
"copilot:read": "copilot is an agent surface that acts on your features through its own typed tool routes; a function-driven copilot turn is not a supported callback lane \u2014 invoke copilot from your app, not from a function",
|
|
7717
|
+
"copilot:write": "copilot is an agent surface that acts on your features through its own typed tool routes; a function-driven copilot turn is not a supported callback lane \u2014 invoke copilot from your app, not from a function"
|
|
7718
|
+
};
|
|
7719
|
+
var CONTROL_PLANE_FUNCTION_SCOPES = ["users:read", "users:write", "usage:read", "audit:read"];
|
|
7720
|
+
var WEBHOOKS_FUNCTION_SCOPES = ["webhooks:read"];
|
|
7657
7721
|
function canonicalFunctionScope(scope) {
|
|
7658
7722
|
const i = scope.indexOf(":");
|
|
7659
7723
|
if (i <= 0) return scope;
|
|
@@ -7663,6 +7727,7 @@ function canonicalFunctionScope(scope) {
|
|
|
7663
7727
|
}
|
|
7664
7728
|
function functionScopeAudience(scope) {
|
|
7665
7729
|
const c = canonicalFunctionScope(scope);
|
|
7730
|
+
if (c in SCOPES_NOT_FOR_FUNCTIONS) return null;
|
|
7666
7731
|
const i = c.indexOf(":");
|
|
7667
7732
|
if (i <= 0) return null;
|
|
7668
7733
|
return FUNCTION_SCOPE_AUDIENCE[c.slice(0, i)] ?? null;
|
|
@@ -7678,8 +7743,30 @@ function functionCallbackAudiences(scopes) {
|
|
|
7678
7743
|
function scopesForFunctionAudience(aud, scopes) {
|
|
7679
7744
|
const canon = [...new Set(scopes.map(canonicalFunctionScope))];
|
|
7680
7745
|
if (aud === "control-plane") return canon.filter((s) => CONTROL_PLANE_FUNCTION_SCOPES.includes(s));
|
|
7746
|
+
if (aud === "webhooks") return canon.filter((s) => WEBHOOKS_FUNCTION_SCOPES.includes(s));
|
|
7681
7747
|
return canon;
|
|
7682
7748
|
}
|
|
7749
|
+
var PUBLIC_KEY_DENY_SCOPES = /* @__PURE__ */ new Set([
|
|
7750
|
+
"admin",
|
|
7751
|
+
"*",
|
|
7752
|
+
"users:read",
|
|
7753
|
+
"users:write",
|
|
7754
|
+
"features:read",
|
|
7755
|
+
"features:write",
|
|
7756
|
+
"secrets:write",
|
|
7757
|
+
// G-15: the tenant-wide audit stream has no end-user owner dimension
|
|
7758
|
+
"audit:read",
|
|
7759
|
+
"functions:read",
|
|
7760
|
+
"functions:write",
|
|
7761
|
+
"jobs:read",
|
|
7762
|
+
"jobs:write",
|
|
7763
|
+
"webhooks:read",
|
|
7764
|
+
"webhooks:write",
|
|
7765
|
+
"ratelimits:write",
|
|
7766
|
+
"payments:write",
|
|
7767
|
+
"notifications:send",
|
|
7768
|
+
"auth:write"
|
|
7769
|
+
]);
|
|
7683
7770
|
var TENANT_SLUG_RE = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
7684
7771
|
var FUNCTION_SOURCE_MAX_BYTES = 512e3;
|
|
7685
7772
|
var FN_PLATFORM_DEFAULT_CPU_MS = 3e4;
|
|
@@ -8727,6 +8814,15 @@ function indexArmNote(rows) {
|
|
|
8727
8814
|
if (rows === null) return "arms inline or through the re-index push runs (live row count unavailable)";
|
|
8728
8815
|
return rows <= EQ_INDEX_INLINE_MAX_ROWS ? `arms inline (${rows} rows)` : `needs reindex (${rows} rows; push runs it)`;
|
|
8729
8816
|
}
|
|
8817
|
+
function writeRolesOf(v) {
|
|
8818
|
+
if (!Array.isArray(v)) return [];
|
|
8819
|
+
return [...new Set(v.filter((r) => typeof r === "string"))].sort();
|
|
8820
|
+
}
|
|
8821
|
+
function writeRolesWidens(declared, remote) {
|
|
8822
|
+
if (remote.length === 0) return false;
|
|
8823
|
+
if (declared.length === 0) return true;
|
|
8824
|
+
return declared.some((r) => !remote.includes(r));
|
|
8825
|
+
}
|
|
8730
8826
|
function endUserAccessOf(v) {
|
|
8731
8827
|
return v === "read" || v === "none" ? v : "readwrite";
|
|
8732
8828
|
}
|
|
@@ -8828,6 +8924,7 @@ async function readRemote2(api) {
|
|
|
8828
8924
|
ownerField: c.owner_field ?? null,
|
|
8829
8925
|
public: c.public === true,
|
|
8830
8926
|
endUserAccess: endUserAccessOf(c.end_user_access),
|
|
8927
|
+
writeRoles: writeRolesOf(c.write_roles),
|
|
8831
8928
|
actions: normalizeActions(c.actions),
|
|
8832
8929
|
fields: Array.isArray(c.fields) ? c.fields : []
|
|
8833
8930
|
});
|
|
@@ -8881,6 +8978,10 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
8881
8978
|
if (v !== void 0 && v !== "readwrite" && v !== "read" && v !== "none") {
|
|
8882
8979
|
throw new Error(`cms.collections.${name}.endUserAccess must be 'readwrite' | 'read' | 'none' (got ${JSON.stringify(v)})`);
|
|
8883
8980
|
}
|
|
8981
|
+
const wr = def.writeRoles;
|
|
8982
|
+
if (wr !== void 0 && (!Array.isArray(wr) || wr.some((r) => typeof r !== "string"))) {
|
|
8983
|
+
throw new Error(`cms.collections.${name}.writeRoles must be an array of role slugs (got ${JSON.stringify(wr)})`);
|
|
8984
|
+
}
|
|
8884
8985
|
}
|
|
8885
8986
|
const read = await readRemote2(api);
|
|
8886
8987
|
if (read.unavailable) {
|
|
@@ -9043,6 +9144,24 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
9043
9144
|
});
|
|
9044
9145
|
}
|
|
9045
9146
|
}
|
|
9147
|
+
const declaredRoles = writeRolesOf(def.writeRoles);
|
|
9148
|
+
if (stableStringify(declaredRoles) !== stableStringify(rc.writeRoles)) {
|
|
9149
|
+
const widens = writeRolesWidens(declaredRoles, rc.writeRoles);
|
|
9150
|
+
const fmt = (r) => r.length ? `[${r.join(", ")}]` : "none (ungated)";
|
|
9151
|
+
if (!widens || allowDestructive) {
|
|
9152
|
+
collChanges.push({
|
|
9153
|
+
collection: name,
|
|
9154
|
+
kind: "set-write-roles",
|
|
9155
|
+
detail: `${name} writeRoles \u2192 ${fmt(declaredRoles)} (was ${fmt(rc.writeRoles)})` + (widens ? " \u2014 WIDENS who may write (full-config reconciliation)" : " (narrows end-user writes)")
|
|
9156
|
+
});
|
|
9157
|
+
} else {
|
|
9158
|
+
collChanges.push({
|
|
9159
|
+
collection: name,
|
|
9160
|
+
kind: "warn",
|
|
9161
|
+
detail: `${name}: writeRoles are ${fmt(rc.writeRoles)} remotely but ${fmt(declaredRoles)} in config \u2014 that WIDENS which end users may write, so it is NOT applied. Declare the remote roles to keep them, or re-run with --allow-destructive to widen.`
|
|
9162
|
+
});
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9046
9165
|
const declaredActions = normalizeActions(def.actions);
|
|
9047
9166
|
if (stableStringify(declaredActions) !== stableStringify(rc.actions)) {
|
|
9048
9167
|
collChanges.push({
|
|
@@ -9116,6 +9235,8 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
9116
9235
|
if (def.public === true) body.public = true;
|
|
9117
9236
|
const access = endUserAccessOf(def.endUserAccess);
|
|
9118
9237
|
if (access !== "readwrite") body.end_user_access = access;
|
|
9238
|
+
const roles = writeRolesOf(def.writeRoles);
|
|
9239
|
+
if (roles.length > 0) body.write_roles = roles;
|
|
9119
9240
|
if (def.actions && def.actions.length > 0) body.actions = normalizeActions(def.actions);
|
|
9120
9241
|
const res = await api("POST", "/v1/cms/collections", body);
|
|
9121
9242
|
if (res.status !== 201 && res.status !== 200 && res.body.error?.code !== "collection_exists") {
|
|
@@ -9163,6 +9284,14 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
9163
9284
|
throw new Error(`cms: set endUserAccess on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
|
|
9164
9285
|
}
|
|
9165
9286
|
applied++;
|
|
9287
|
+
} else if (c.kind === "set-write-roles") {
|
|
9288
|
+
const target = writeRolesOf(collections[c.collection].writeRoles);
|
|
9289
|
+
const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, { write_roles: target });
|
|
9290
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
9291
|
+
const e = res.body.error ?? {};
|
|
9292
|
+
throw new Error(`cms: set writeRoles on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
|
|
9293
|
+
}
|
|
9294
|
+
applied++;
|
|
9166
9295
|
} else if (c.kind === "set-actions") {
|
|
9167
9296
|
const target = normalizeActions(collections[c.collection].actions);
|
|
9168
9297
|
const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, { actions: target });
|
|
@@ -9228,7 +9357,7 @@ function formatCmsChanges(changes) {
|
|
|
9228
9357
|
if (c.kind === "destructive") return ` ! ${c.detail}`;
|
|
9229
9358
|
if (c.kind === "warn") return ` \u26A0 ${c.detail}`;
|
|
9230
9359
|
if (c.kind === "reindex") return ` \u21BB ${c.detail}`;
|
|
9231
|
-
if (c.kind === "alter-flags" || c.kind === "set-owner-field" || c.kind === "set-end-user-access") return ` ~ ${c.detail}`;
|
|
9360
|
+
if (c.kind === "alter-flags" || c.kind === "set-owner-field" || c.kind === "set-end-user-access" || c.kind === "set-write-roles") return ` ~ ${c.detail}`;
|
|
9232
9361
|
return ` + ${c.detail}`;
|
|
9233
9362
|
}).join("\n");
|
|
9234
9363
|
}
|
|
@@ -9407,7 +9536,7 @@ var API_CHEATSHEET = [
|
|
|
9407
9536
|
// realtime
|
|
9408
9537
|
{ feature: "realtime", what: "publish an event to a channel", cmd: `vxil api POST /v1/realtime/channels/<channel>/publish '{"event":"ping","data":{}}'` },
|
|
9409
9538
|
// webhooks
|
|
9410
|
-
{ feature: "webhooks", what: "inbound sources add / list / rm / test", cmd: "vxil webhooks sources add --provider stripe --name prod [--forward-url https://\u2026] \xB7 sources list \xB7 sources rm <id> [--yes] \xB7 sources test <id>" },
|
|
9539
|
+
{ feature: "webhooks", what: "inbound sources add / list / rm / test", cmd: "vxil webhooks sources add --provider stripe --name prod [--forward-url https://\u2026 | --target-function <fn>] [--provider hmac --verify <json>] \xB7 sources list \xB7 sources rm <id> [--yes] \xB7 sources test <id>" },
|
|
9411
9540
|
{ feature: "webhooks", what: "replay an inbound event / see its delivery runs", cmd: "vxil webhooks events replay <event_id> \xB7 vxil webhooks events runs <event_id>" },
|
|
9412
9541
|
{ feature: "webhooks", what: "declare outbound subscriptions in vxil.config (converged by push)", cmd: "features.webhooks.subscriptions[] in vxil.config.ts \u2192 vxil push" },
|
|
9413
9542
|
{ feature: "webhooks", what: "add an outbound subscription by hand (undeclared \u2014 `vxil diff --strict-api-state` reports it)", cmd: `vxil api POST /v1/webhooks/subscriptions '{"target_url":"https://\u2026","event_prefixes":["cms."]}'` },
|
|
@@ -10789,7 +10918,8 @@ async function filesRm(api, objectId) {
|
|
|
10789
10918
|
if (!ok2xx(r)) throw new Error(`could not delete '${objectId}' (${apiErrText(r)})`);
|
|
10790
10919
|
return { object_id: objectId, deleted: true };
|
|
10791
10920
|
}
|
|
10792
|
-
var WEBHOOK_PROVIDERS = ["stripe", "paddle", "github", "slack", "revenuecat", "generic"];
|
|
10921
|
+
var WEBHOOK_PROVIDERS = ["stripe", "paddle", "github", "slack", "revenuecat", "generic", "hmac"];
|
|
10922
|
+
var TARGET_FUNCTION_RE = /^[a-z][a-z0-9-]{0,47}$/;
|
|
10793
10923
|
function parseSourceAdd(f) {
|
|
10794
10924
|
const provider = (f.provider ?? "").trim();
|
|
10795
10925
|
if (!WEBHOOK_PROVIDERS.includes(provider)) {
|
|
@@ -10800,14 +10930,48 @@ function parseSourceAdd(f) {
|
|
|
10800
10930
|
if (name.length > 200) return { error: `--name is at most 200 characters (got ${name.length})` };
|
|
10801
10931
|
const fwd = f.forwardUrl?.trim();
|
|
10802
10932
|
if (fwd !== void 0 && fwd !== "" && !/^https:\/\//i.test(fwd)) return { error: "--forward-url must be an https:// URL" };
|
|
10803
|
-
|
|
10933
|
+
const fn = f.targetFunction?.trim();
|
|
10934
|
+
if (fn !== void 0 && fn !== "") {
|
|
10935
|
+
if (!TARGET_FUNCTION_RE.test(fn)) return { error: `--target-function must be a function name matching ${TARGET_FUNCTION_RE.source}` };
|
|
10936
|
+
if (fwd) return { error: "set --forward-url OR --target-function, not both (one source, one destination)" };
|
|
10937
|
+
}
|
|
10938
|
+
let verify;
|
|
10939
|
+
const rawVerify = f.verify?.trim();
|
|
10940
|
+
if (provider === "hmac") {
|
|
10941
|
+
if (!rawVerify) {
|
|
10942
|
+
return { error: `--provider hmac needs --verify '<json>', e.g. --verify '{"header":"X-Shopify-Hmac-Sha256","encoding":"base64"}'` };
|
|
10943
|
+
}
|
|
10944
|
+
try {
|
|
10945
|
+
const v = JSON.parse(rawVerify);
|
|
10946
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return { error: "--verify must be a JSON object" };
|
|
10947
|
+
verify = v;
|
|
10948
|
+
} catch {
|
|
10949
|
+
return { error: "--verify must be valid JSON (a single-quoted object)" };
|
|
10950
|
+
}
|
|
10951
|
+
} else if (rawVerify) {
|
|
10952
|
+
return { error: "--verify is accepted only with --provider hmac" };
|
|
10953
|
+
}
|
|
10954
|
+
return {
|
|
10955
|
+
body: {
|
|
10956
|
+
provider,
|
|
10957
|
+
name,
|
|
10958
|
+
...fwd ? { forward_url: fwd } : {},
|
|
10959
|
+
...fn ? { target_function: fn } : {},
|
|
10960
|
+
...verify ? { verify } : {}
|
|
10961
|
+
}
|
|
10962
|
+
};
|
|
10804
10963
|
}
|
|
10805
10964
|
async function sourcesAdd(api, body) {
|
|
10806
10965
|
const r = await api("POST", "/v1/webhooks/sources", body);
|
|
10807
10966
|
if (!ok2xx(r)) throw new Error(`could not register the source (${apiErrText(r)})`);
|
|
10808
10967
|
const d = dataOf(r);
|
|
10809
10968
|
if (!d.source_id || !d.receiver_url_path) throw new Error("the source route returned no source_id/receiver_url_path");
|
|
10810
|
-
return {
|
|
10969
|
+
return {
|
|
10970
|
+
source_id: d.source_id,
|
|
10971
|
+
receiver_url_path: d.receiver_url_path,
|
|
10972
|
+
provider: d.provider ?? body.provider,
|
|
10973
|
+
...body.target_function ? { target_function: d.target_function ?? body.target_function } : {}
|
|
10974
|
+
};
|
|
10811
10975
|
}
|
|
10812
10976
|
function sourceAddStdout(s, edgeBase, json2) {
|
|
10813
10977
|
if (json2) return JSON.stringify({ source_id: s.source_id, provider: s.provider, receiver_url_path: s.receiver_url_path, receiver_url: `${edgeBase.replace(/\/$/, "")}${s.receiver_url_path}` });
|
|
@@ -10816,9 +10980,14 @@ function sourceAddStdout(s, edgeBase, json2) {
|
|
|
10816
10980
|
function sourceAddNotices(s) {
|
|
10817
10981
|
return [
|
|
10818
10982
|
`\u2713 registered ${s.provider} source ${s.source_id} \u2014 the receiver URL above is shown ONCE; paste it into the provider's webhook settings`,
|
|
10819
|
-
`a signing secret goes in \`vxil secrets set webhooks/source_${s.source_id}\` (a 'generic' source needs none); \`vxil listen --source ${s.source_id} --forward-to <url>\` forwards its events locally
|
|
10983
|
+
`a signing secret goes in \`vxil secrets set webhooks/source_${s.source_id.toLowerCase()}\` (a 'generic' source needs none); \`vxil listen --source ${s.source_id} --forward-to <url>\` forwards its events locally`,
|
|
10984
|
+
...s.target_function ? [`verified events are delivered to function ${s.target_function}: it needs a bare webhook binding (triggers: [{ kind: 'webhook' }]); a cron-only function is woken as a cron tick WITHOUT the event, and a function with neither binding skips it`] : []
|
|
10820
10985
|
];
|
|
10821
10986
|
}
|
|
10987
|
+
function sourceDestination(s) {
|
|
10988
|
+
if (s.target_function) return `fn:${s.target_function}`;
|
|
10989
|
+
return s.forward_url || null;
|
|
10990
|
+
}
|
|
10822
10991
|
async function sourcesList(api) {
|
|
10823
10992
|
const r = await api("GET", "/v1/webhooks/sources");
|
|
10824
10993
|
if (!ok2xx(r)) throw new Error(`could not list the sources (${apiErrText(r)})`);
|
|
@@ -11107,10 +11276,20 @@ function boundedData(data2, maxBytes, onTruncate) {
|
|
|
11107
11276
|
const bytes = new TextEncoder().encode(text).length;
|
|
11108
11277
|
if (bytes > maxBytes) {
|
|
11109
11278
|
onTruncate?.(bytes);
|
|
11110
|
-
return
|
|
11279
|
+
return truncatedStub(data2);
|
|
11111
11280
|
}
|
|
11112
11281
|
return data2;
|
|
11113
11282
|
}
|
|
11283
|
+
function truncatedStub(data2) {
|
|
11284
|
+
const stub = { truncated: true };
|
|
11285
|
+
if (data2 && typeof data2 === "object" && !Array.isArray(data2)) {
|
|
11286
|
+
for (const k of ["vxil_event_id", "source_id"]) {
|
|
11287
|
+
const v = data2[k];
|
|
11288
|
+
if (typeof v === "string" && /^[A-Za-z0-9_]{1,64}$/.test(v)) stub[k] = v;
|
|
11289
|
+
}
|
|
11290
|
+
}
|
|
11291
|
+
return stub;
|
|
11292
|
+
}
|
|
11114
11293
|
function webhookPayload(inner, maxBytes = WEBHOOK_PAYLOAD_MAX_BYTES) {
|
|
11115
11294
|
return {
|
|
11116
11295
|
event: inner.webhook_event,
|
|
@@ -11361,6 +11540,7 @@ function controlPlaneFunctionScope(method, pathname) {
|
|
|
11361
11540
|
const read = m === "GET" || m === "HEAD";
|
|
11362
11541
|
if (pathname === "/v1/users" || pathname.startsWith("/v1/users/")) return read ? "users:read" : "users:write";
|
|
11363
11542
|
if (pathname === "/v1/usage") return read ? "usage:read" : null;
|
|
11543
|
+
if (pathname === "/v1/audit" || pathname === "/v1/audit/export") return read ? "audit:read" : null;
|
|
11364
11544
|
return null;
|
|
11365
11545
|
}
|
|
11366
11546
|
function json(body, status, headers = {}) {
|
|
@@ -11774,6 +11954,16 @@ export type VxilFilterJson = string | number | boolean | { $eq?: string | number
|
|
|
11774
11954
|
}
|
|
11775
11955
|
|
|
11776
11956
|
// ../../workers/mcp-v1/src/tools.ts
|
|
11957
|
+
function auditExportQuery(a) {
|
|
11958
|
+
const qs = new URLSearchParams();
|
|
11959
|
+
for (const k of ["subject", "since", "until", "after_id"]) {
|
|
11960
|
+
const v = a[k];
|
|
11961
|
+
if (typeof v === "string" && v) qs.set(k, v);
|
|
11962
|
+
}
|
|
11963
|
+
const n = Number(a.limit);
|
|
11964
|
+
qs.set("limit", String(Number.isFinite(n) ? Math.min(Math.max(Math.trunc(n), 1), 1e3) : 500));
|
|
11965
|
+
return qs.toString();
|
|
11966
|
+
}
|
|
11777
11967
|
var TOOLS = [
|
|
11778
11968
|
{
|
|
11779
11969
|
name: "users_upsert",
|
|
@@ -12278,7 +12468,7 @@ var TOOLS = [
|
|
|
12278
12468
|
{
|
|
12279
12469
|
name: "jobs_replay_run",
|
|
12280
12470
|
feature: "jobs",
|
|
12281
|
-
description: "Replay a finished plain run (dead / failed / cancelled / succeeded): a NEW run with the same job, target and payload is queued (replayed_from names the original). The target receives it like any delivery \u2014 dedupe on run_id if it must not act twice. A generation run is not replayable (409 not_replayable): enqueue the generation again.",
|
|
12471
|
+
description: "Replay a finished plain run (dead / failed / cancelled / succeeded): a NEW run with the same job, target and payload is queued (replayed_from names the original). The target receives it like any delivery \u2014 dedupe on run_id if it must not act twice. A generation run is not replayable (409 not_replayable): enqueue the generation again. A redacted or erased run is not replayable either (409 run_redacted): its payload is gone.",
|
|
12282
12472
|
inputSchema: {
|
|
12283
12473
|
type: "object",
|
|
12284
12474
|
properties: { run_id: { type: "string" } },
|
|
@@ -12287,6 +12477,20 @@ var TOOLS = [
|
|
|
12287
12477
|
method: "POST",
|
|
12288
12478
|
path: (a) => `/v1/jobs/runs/${encodeURIComponent(String(a.run_id))}/replay`
|
|
12289
12479
|
},
|
|
12480
|
+
{
|
|
12481
|
+
name: "jobs_redact_runs",
|
|
12482
|
+
feature: "jobs",
|
|
12483
|
+
description: "Redact FINISHED runs that hold personal data (a privacy request for runs nothing links to a user, e.g. a plain queue run or an async function call): payload becomes { erased: true }; result, progress, callback body and error text are cleared; a generation's provider request body/headers are dropped and its credit hold's user id is pseudonymized (unless its settle is still owed). The run row stays (state, timings, job_name). Send EXACTLY ONE of run_ids (\u2264500; open runs are left alone and counted in skipped_open) or user_id (the generation runs whose credit hold names that user \u2014 one page of \u2264500 per call; done:false \u2192 call again). Idempotent; irreversible; a redacted run no longer replays. concurrency_key, debounce_key, idempotency_key and target_url are NOT cleared \u2014 keep user ids out of them. Answers { redacted, skipped_open, done }.",
|
|
12484
|
+
inputSchema: {
|
|
12485
|
+
type: "object",
|
|
12486
|
+
properties: {
|
|
12487
|
+
run_ids: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 500, description: "Finished runs to redact." },
|
|
12488
|
+
user_id: { type: "string", description: "Redact the finished generation runs whose credit hold names this end-user." }
|
|
12489
|
+
}
|
|
12490
|
+
},
|
|
12491
|
+
method: "POST",
|
|
12492
|
+
path: "/v1/jobs/runs/redact"
|
|
12493
|
+
},
|
|
12290
12494
|
{
|
|
12291
12495
|
name: "jobs_emit_event",
|
|
12292
12496
|
feature: "jobs",
|
|
@@ -12645,7 +12849,7 @@ var TOOLS = [
|
|
|
12645
12849
|
{
|
|
12646
12850
|
name: "cms_list_collections",
|
|
12647
12851
|
feature: "cms",
|
|
12648
|
-
description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value. Each collection row also carries end_user_access: 'readwrite' (default), 'read' (a verified end user \u2014 a thin-client key with a session \u2014 may read but every write answers 403 server_only) or 'none' (end-user reads and writes are 403 server_only); server keys are never affected.",
|
|
12852
|
+
description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value. Each collection row also carries end_user_access: 'readwrite' (default), 'read' (a verified end user \u2014 a thin-client key with a session \u2014 may read but every write answers 403 server_only) or 'none' (end-user reads and writes are 403 server_only), and write_roles: the roles a verified end user must hold to WRITE the collection (null = ungated; without one every end-user write answers 403 role_required, reads are unaffected); server keys are never affected. retain_days (null = keep) is the collection's retention: the nightly platform sweep soft-deletes live items created more than that many days ago.",
|
|
12649
12853
|
inputSchema: { type: "object", properties: {} },
|
|
12650
12854
|
method: "GET",
|
|
12651
12855
|
path: "/v1/cms/collections"
|
|
@@ -12653,13 +12857,23 @@ var TOOLS = [
|
|
|
12653
12857
|
{
|
|
12654
12858
|
name: "cms_create_item",
|
|
12655
12859
|
feature: "cms",
|
|
12656
|
-
description: "Create a content item in a collection. data must match the collection field defs (call cms_list_collections first). status draft|published.",
|
|
12860
|
+
description: "Create a content item in a collection. data must match the collection field defs (call cms_list_collections first). status draft|published. Upsert: pass onConflict {field, patch} where field is a unique field present in data \u2014 if a live item already holds that value, patch is merged into it instead (200, audited and hooked as an update; null clears a field), else the item is created (201).",
|
|
12657
12861
|
inputSchema: {
|
|
12658
12862
|
type: "object",
|
|
12659
12863
|
properties: {
|
|
12660
12864
|
collection: { type: "string" },
|
|
12661
12865
|
data: { type: "object", additionalProperties: true },
|
|
12662
|
-
status: { type: "string", enum: ["draft", "published"], default: "draft" }
|
|
12866
|
+
status: { type: "string", enum: ["draft", "published"], default: "draft" },
|
|
12867
|
+
onConflict: {
|
|
12868
|
+
type: "object",
|
|
12869
|
+
description: "Upsert by a declared unique field: { field: <unique field in data>, patch: { \u2026merge-patch applied to the existing item } }.",
|
|
12870
|
+
properties: {
|
|
12871
|
+
field: { type: "string" },
|
|
12872
|
+
patch: { type: "object", additionalProperties: true }
|
|
12873
|
+
},
|
|
12874
|
+
required: ["field", "patch"],
|
|
12875
|
+
additionalProperties: false
|
|
12876
|
+
}
|
|
12663
12877
|
},
|
|
12664
12878
|
required: ["collection", "data"]
|
|
12665
12879
|
},
|
|
@@ -12669,7 +12883,7 @@ var TOOLS = [
|
|
|
12669
12883
|
{
|
|
12670
12884
|
name: "cms_query_items",
|
|
12671
12885
|
feature: "cms",
|
|
12672
|
-
description: 'Query content items with the bounded DSL. filter is a JSON object (\u22648 terms): {"price":{"$gte":10},"status":"published"}; ops $eq $ne $gt $gte $lt $lte $in $contains $startsWith (substring/prefix, LIKE-escaped) $arrayContains/$anyOf (array fields: all-of / any-of). Equality ($eq / bare value / $in) is index-served on slot-indexed and `indexed` fields; on other fields it is a bounded scan (same results). sort: "-field" (slot-indexed fields + created_at/updated_at/published_at). Paging: pass the returned next_cursor back as cursor WITH THE SAME filter and sort (works for the default order and for any slot/timestamp sort; not for a dotted join sort). $expand inlines relation/file fields. count=true returns {count} instead of a page (filter only \u2014 no sort/limit/cursor/$expand).',
|
|
12886
|
+
description: 'Query content items with the bounded DSL. filter is a JSON object (\u22648 terms): {"price":{"$gte":10},"status":"published"}; ops $eq $ne $gt $gte $lt $lte $in ($ne counts an absent/null field as not equal) $contains $startsWith (substring/prefix, LIKE-escaped) $arrayContains/$anyOf (array fields: all-of / any-of). Equality ($eq / bare value / $in) is index-served on slot-indexed and `indexed` fields; on other fields it is a bounded scan (same results). sort: "-field" (slot-indexed fields + created_at/updated_at/published_at). Paging: pass the returned next_cursor back as cursor WITH THE SAME filter and sort (works for the default order and for any slot/timestamp sort; not for a dotted join sort). $expand inlines relation/file fields. count=true returns {count} instead of a page (filter only \u2014 no sort/limit/cursor/$expand).',
|
|
12673
12887
|
inputSchema: {
|
|
12674
12888
|
type: "object",
|
|
12675
12889
|
properties: {
|
|
@@ -13140,6 +13354,46 @@ var TOOLS = [
|
|
|
13140
13354
|
method: "DELETE",
|
|
13141
13355
|
path: (a) => `/v1/functions/${encodeURIComponent(String(a.name))}`
|
|
13142
13356
|
},
|
|
13357
|
+
{
|
|
13358
|
+
name: "functions_list",
|
|
13359
|
+
feature: "functions",
|
|
13360
|
+
description: "The project's deployed functions: name, endpoint (/v1/fn/<name>), trigger bindings, scopes, limits and the runtime each runs under, plus `enabled` (false while the project's functions are paused as a whole \u2014 invokes then answer 404) and `cpuMsCeiling`. Read-only; any authenticated key may read it. Use it before functions_invoke or functions_logs to get the exact names.",
|
|
13361
|
+
inputSchema: { type: "object", properties: {} },
|
|
13362
|
+
method: "GET",
|
|
13363
|
+
path: "/v1/functions"
|
|
13364
|
+
},
|
|
13365
|
+
{
|
|
13366
|
+
name: "functions_invoke",
|
|
13367
|
+
feature: "functions",
|
|
13368
|
+
description: "Invoke ONE deployed function synchronously (POST /v1/fn/<name>) and return its own response verbatim (not a {data, meta} envelope). Every argument other than `name` is sent as the JSON request body, which the function reads as its payload. At-least-once semantics are the function's own: send an `idempotency_key` argument only if the function reads one from its payload. 404 when the name is not deployed; 403 without functions:invoke. Runs tenant code \u2014 it is metered like any invoke.",
|
|
13369
|
+
inputSchema: {
|
|
13370
|
+
type: "object",
|
|
13371
|
+
properties: {
|
|
13372
|
+
name: { type: "string", description: "The deployed function name (lowercase, e.g. on-payment)." }
|
|
13373
|
+
},
|
|
13374
|
+
required: ["name"],
|
|
13375
|
+
additionalProperties: true
|
|
13376
|
+
},
|
|
13377
|
+
method: "POST",
|
|
13378
|
+
path: (a) => `/v1/fn/${encodeURIComponent(String(a.name))}`,
|
|
13379
|
+
pathArgs: ["name"]
|
|
13380
|
+
},
|
|
13381
|
+
{
|
|
13382
|
+
name: "functions_logs",
|
|
13383
|
+
feature: "functions",
|
|
13384
|
+
description: "Log records of ONE deployed function, one per invocation (ts, outcome, wall_ms, cpu_ms, console lines, exceptions), oldest first, with `next_since` \u2014 pass it back as `since` to read only newer records (a tail). Without `since`: the most recent records of the last 24 hours. Kept 14 days. Needs functions:read.",
|
|
13385
|
+
inputSchema: {
|
|
13386
|
+
type: "object",
|
|
13387
|
+
properties: {
|
|
13388
|
+
name: { type: "string", description: "The deployed function name." },
|
|
13389
|
+
since: { type: "string", description: "ISO timestamp: only lines after it (the previous call's next_since)." }
|
|
13390
|
+
},
|
|
13391
|
+
required: ["name"]
|
|
13392
|
+
},
|
|
13393
|
+
method: "GET",
|
|
13394
|
+
path: (a) => `/v1/functions/${encodeURIComponent(String(a.name))}/logs`,
|
|
13395
|
+
queryArgs: ["since"]
|
|
13396
|
+
},
|
|
13143
13397
|
{
|
|
13144
13398
|
name: "webhooks_test_subscription",
|
|
13145
13399
|
feature: "webhooks",
|
|
@@ -13155,7 +13409,7 @@ var TOOLS = [
|
|
|
13155
13409
|
{
|
|
13156
13410
|
name: "webhooks_test_source",
|
|
13157
13411
|
feature: "webhooks",
|
|
13158
|
-
description: "Send a sample inbound envelope to a source's forward_url through the real delivery path (jobs signing + SSRF guard). 200 \u2192 inline outcome; 202 \u2192 still in flight; 409 no_forward_url when the source has
|
|
13412
|
+
description: "Send a sample inbound envelope to a source's destination (its forward_url, or its target_function) through the real delivery path (jobs signing + SSRF guard). 200 \u2192 inline outcome; 202 \u2192 still in flight; 409 no_forward_url when the source has neither.",
|
|
13159
13413
|
inputSchema: {
|
|
13160
13414
|
type: "object",
|
|
13161
13415
|
properties: { source_id: { type: "string" } },
|
|
@@ -13184,6 +13438,25 @@ var TOOLS = [
|
|
|
13184
13438
|
method: "GET",
|
|
13185
13439
|
path: "/v1/webhooks/events/catalog"
|
|
13186
13440
|
},
|
|
13441
|
+
{
|
|
13442
|
+
name: "webhooks_list_events",
|
|
13443
|
+
feature: "webhooks",
|
|
13444
|
+
description: "Received INBOUND webhook events (what providers POSTed to your sources): event id, source, provider, provider event id, type, payload, whether the signature verified, and the forward status. Newest first, paged with `cursor` (next_cursor); or pass `after` (an event id \u2014 your watermark) to read OLDEST first from just after it, storing `next_after` (has_more says another page waits; the after drain answers only events at least 10 s old, so a stored watermark never skips a late-committing event). `event_id` reads exactly one event. Filter by source_id and status. Needs webhooks:read.",
|
|
13445
|
+
inputSchema: {
|
|
13446
|
+
type: "object",
|
|
13447
|
+
properties: {
|
|
13448
|
+
source_id: { type: "string" },
|
|
13449
|
+
status: { type: "string", enum: ["received", "forwarding", "forwarded"] },
|
|
13450
|
+
cursor: { type: "string", description: "next_cursor of the previous (newest-first) page." },
|
|
13451
|
+
after: { type: "string", description: "An event id: answer the events after it, oldest first. Exclusive with cursor." },
|
|
13452
|
+
event_id: { type: "string", description: "Exactly this event." },
|
|
13453
|
+
limit: { type: "number", default: 50, description: "1..200" }
|
|
13454
|
+
}
|
|
13455
|
+
},
|
|
13456
|
+
method: "GET",
|
|
13457
|
+
path: "/v1/webhooks/events",
|
|
13458
|
+
queryArgs: ["source_id", "status", "cursor", "after", "event_id", "limit"]
|
|
13459
|
+
},
|
|
13187
13460
|
{
|
|
13188
13461
|
name: "feeds_add_activity",
|
|
13189
13462
|
feature: "activity-feed",
|
|
@@ -13452,7 +13725,8 @@ var TOOLS = [
|
|
|
13452
13725
|
provider: { type: "string" },
|
|
13453
13726
|
model: { type: "string" },
|
|
13454
13727
|
input: { type: "object", additionalProperties: true, description: "Extra template vars beyond {{query}}/{{context}}." },
|
|
13455
|
-
user_id: { type: "string", description: "Opaque end_user_id for per-user usage metering." }
|
|
13728
|
+
user_id: { type: "string", description: "Opaque end_user_id for per-user usage metering." },
|
|
13729
|
+
min_similarity: { type: "number", minimum: -1, maximum: 1, description: "Ground only on chunks whose cosine similarity to the query is at least this (-1..1). Overrides rag config retrieval.minSimilarity." }
|
|
13456
13730
|
},
|
|
13457
13731
|
required: ["query"]
|
|
13458
13732
|
},
|
|
@@ -13462,7 +13736,7 @@ var TOOLS = [
|
|
|
13462
13736
|
{
|
|
13463
13737
|
name: "rag_search",
|
|
13464
13738
|
feature: "rag",
|
|
13465
|
-
description: "Retrieval-only grounding preview: the exact chunks rag_answer would ground on, with rerank + metadata boosts applied \u2014 no generation, no token spend. Use it to inspect/tune retrieval (boost weights, rerank,
|
|
13739
|
+
description: "Retrieval-only grounding preview: the exact chunks rag_answer would ground on, with rerank + metadata boosts applied \u2014 no generation, no token spend. Use it to inspect/tune retrieval (boost weights, rerank, min_similarity) before paying for an answer.",
|
|
13466
13740
|
inputSchema: {
|
|
13467
13741
|
type: "object",
|
|
13468
13742
|
properties: {
|
|
@@ -13473,7 +13747,8 @@ var TOOLS = [
|
|
|
13473
13747
|
filter: { type: "object", additionalProperties: true, description: "Metadata equality filter applied to retrieval." },
|
|
13474
13748
|
rerank: { type: "boolean", description: "Forward rerank:true to vector-search's BYO rerank pass (inert until configured there)." },
|
|
13475
13749
|
boosts: { type: "object", additionalProperties: true, description: "Per-metadata-field boost specs: { field: { kind:'recency', halfLifeDays?, weight? } | { kind:'value', weights:{\u2026} } }." },
|
|
13476
|
-
min_score: { type: "number", description: "Drop hits below this EFFECTIVE (post-boost) score. score is a rank value (about 0.016-0.033 at the top), so a floor above ~0.033 drops every hit;
|
|
13750
|
+
min_score: { type: "number", description: "Drop hits below this EFFECTIVE (post-boost) score. score is a rank value (about 0.016-0.033 at the top), so a floor above ~0.033 drops every hit; use min_similarity for a relevance threshold." },
|
|
13751
|
+
min_similarity: { type: "number", minimum: -1, maximum: 1, description: "Drop hits whose cosine similarity to the query is below this (-1..1), e.g. 0.5. Overrides rag config retrieval.minSimilarity; a hit with no measured similarity (keyword mode) is kept." }
|
|
13477
13752
|
},
|
|
13478
13753
|
required: ["query"]
|
|
13479
13754
|
},
|
|
@@ -13594,6 +13869,41 @@ var TOOLS = [
|
|
|
13594
13869
|
method: "GET",
|
|
13595
13870
|
path: "/v1/usage"
|
|
13596
13871
|
},
|
|
13872
|
+
{
|
|
13873
|
+
name: "audit_query",
|
|
13874
|
+
description: "The project's audit trail, newest first: every write and lifecycle event (id, event, actor, surface, request_id, payload, created_at), paged with `cursor` (next_cursor). `subject` narrows to the events about ONE thing: a user id (payload.user_id / payload.id), a cms record (payload.item_id \u2014 the record's history), a verified end user (payload.end_user_id \u2014 what that person changed through a thin-client key or a function acting for them, and their payments events), or a principal (actor). Read-only; server keys only (refused in end-user mode). Needs audit:read or features:read.",
|
|
13875
|
+
inputSchema: {
|
|
13876
|
+
type: "object",
|
|
13877
|
+
properties: {
|
|
13878
|
+
subject: { type: "string", description: "A user id, cms item id, end-user id or actor to filter by." },
|
|
13879
|
+
since: { type: "string", description: "ISO timestamp lower bound." },
|
|
13880
|
+
cursor: { type: "string", description: "next_cursor of the previous page." },
|
|
13881
|
+
limit: { type: "number", default: 50, description: "1..200" }
|
|
13882
|
+
}
|
|
13883
|
+
},
|
|
13884
|
+
method: "GET",
|
|
13885
|
+
path: "/v1/audit",
|
|
13886
|
+
queryArgs: ["subject", "since", "cursor", "limit"]
|
|
13887
|
+
},
|
|
13888
|
+
{
|
|
13889
|
+
name: "audit_export",
|
|
13890
|
+
description: "Export the audit trail OLDEST first as NDJSON (one event per line) for a window \u2014 the evidence/compliance read. Resume with `after_id` = the id of the last line. Same `subject` filter as audit_query. At most 1000 rows per call from this tool (default 500) so the answer fits an agent's context; page with after_id. Read-only; server keys only. Needs audit:read or features:read.",
|
|
13891
|
+
inputSchema: {
|
|
13892
|
+
type: "object",
|
|
13893
|
+
properties: {
|
|
13894
|
+
subject: { type: "string", description: "A user id, cms item id, end-user id or actor to filter by." },
|
|
13895
|
+
since: { type: "string", description: "ISO timestamp lower bound (inclusive)." },
|
|
13896
|
+
until: { type: "string", description: "ISO timestamp upper bound (exclusive)." },
|
|
13897
|
+
after_id: { type: "string", description: "Continue after this audit id (the last line of the previous call)." },
|
|
13898
|
+
limit: { type: "number", default: 500, description: "1..1000" }
|
|
13899
|
+
}
|
|
13900
|
+
},
|
|
13901
|
+
method: "GET",
|
|
13902
|
+
// The REST route allows 10 000 rows; an MCP answer is model context, so
|
|
13903
|
+
// the tool clamps the page (auditExportQuery builds the whole query; the
|
|
13904
|
+
// path stays one literal so the trinity-drift scanner reads the route).
|
|
13905
|
+
path: (a) => `/v1/audit/export?${auditExportQuery(a)}`
|
|
13906
|
+
},
|
|
13597
13907
|
{
|
|
13598
13908
|
name: "copilot_send_message",
|
|
13599
13909
|
feature: "copilot",
|
|
@@ -14289,6 +14599,22 @@ function parseKeyEnv(raw) {
|
|
|
14289
14599
|
if (raw === "live" || raw === "test") return { env: raw };
|
|
14290
14600
|
return { error: `--env must be 'live' or 'test' (got '${raw}') \u2014 this is the key's env prefix, not the project's environment label` };
|
|
14291
14601
|
}
|
|
14602
|
+
function parseKeyClass(rawClass, endUserRequired) {
|
|
14603
|
+
if (rawClass !== void 0 && rawClass !== "server" && rawClass !== "public") {
|
|
14604
|
+
return { error: `--class must be 'server' or 'public' (got '${rawClass}') \u2014 public = a thin-client key for a browser / mobile app (end_user_required)` };
|
|
14605
|
+
}
|
|
14606
|
+
if (rawClass === "server" && endUserRequired) {
|
|
14607
|
+
return { error: "--end-user-required makes a public (thin-client) key \u2014 drop it, or use --class public" };
|
|
14608
|
+
}
|
|
14609
|
+
return { keyClass: endUserRequired ? "public" : rawClass ?? "server" };
|
|
14610
|
+
}
|
|
14611
|
+
function checkPublicKeyScopes(scopes) {
|
|
14612
|
+
const denied = scopes.filter((s) => PUBLIC_KEY_DENY_SCOPES.has(s));
|
|
14613
|
+
if (!denied.length) return null;
|
|
14614
|
+
return {
|
|
14615
|
+
error: `a public (thin-client) key cannot carry server-only scope${denied.length > 1 ? "s" : ""}: ${denied.join(", ")} \u2014 keep the public key to end-user data scopes (e.g. cms:read,cms:write,files:write,realtime:write,auth:signin) and mint a separate server key for your backend`
|
|
14616
|
+
};
|
|
14617
|
+
}
|
|
14292
14618
|
function parseKeyName(raw) {
|
|
14293
14619
|
const name = (raw ?? "").trim();
|
|
14294
14620
|
if (!name) return { error: "--name is required \u2014 a label for this key, e.g. --name web-worker" };
|
|
@@ -14299,7 +14625,7 @@ async function mintKeyViaSession(dash, cookie, tenantId, req) {
|
|
|
14299
14625
|
const r = await dash(
|
|
14300
14626
|
"POST",
|
|
14301
14627
|
`/dashboard/tenants/${tenantId}/api-keys`,
|
|
14302
|
-
{ name: req.name, scopes: req.scopes, env: req.env },
|
|
14628
|
+
{ name: req.name, scopes: req.scopes, env: req.env, ...req.keyClass === "public" ? { end_user_required: true } : {} },
|
|
14303
14629
|
cookie
|
|
14304
14630
|
);
|
|
14305
14631
|
if (r.status === 401) return { unauthorized: true };
|
|
@@ -14315,7 +14641,10 @@ async function mintKeyViaSession(dash, cookie, tenantId, req) {
|
|
|
14315
14641
|
scopes: d.scopes ?? req.scopes,
|
|
14316
14642
|
env: d.env ?? req.env,
|
|
14317
14643
|
name: req.name,
|
|
14318
|
-
...d.prefix ? { prefix: d.prefix } : {}
|
|
14644
|
+
...d.prefix ? { prefix: d.prefix } : {},
|
|
14645
|
+
// report ONLY what the server says it minted: a stale server that
|
|
14646
|
+
// ignored the flag (and so omits the field) must not read as public
|
|
14647
|
+
...d.end_user_required === true ? { end_user_required: true } : {}
|
|
14319
14648
|
}
|
|
14320
14649
|
};
|
|
14321
14650
|
}
|
|
@@ -14386,7 +14715,7 @@ function revokeGuard(args) {
|
|
|
14386
14715
|
return { ok: true, row: row2, isCurrent };
|
|
14387
14716
|
}
|
|
14388
14717
|
function mintStdout(r, json2) {
|
|
14389
|
-
return json2 ? JSON.stringify({ api_key: r.api_key, key_id: r.key_id, scopes: r.scopes }) : r.api_key;
|
|
14718
|
+
return json2 ? JSON.stringify({ api_key: r.api_key, key_id: r.key_id, scopes: r.scopes, ...r.end_user_required ? { end_user_required: true } : {} }) : r.api_key;
|
|
14390
14719
|
}
|
|
14391
14720
|
function mintNotices(r, json2, opts = {}) {
|
|
14392
14721
|
const storage = opts.stored ? "It was stored as this project's CLI key (~/.vxil/credentials.json), replacing the previous one." : "It was NOT stored on this machine (pass --store to make it this project's CLI key).";
|
|
@@ -14398,7 +14727,8 @@ function mintNotices(r, json2, opts = {}) {
|
|
|
14398
14727
|
];
|
|
14399
14728
|
}
|
|
14400
14729
|
return [
|
|
14401
|
-
`\u2713 minted '${r.name}' (${r.key_id}, env ${r.env}) with scopes [${r.scopes.join(", ")}]`,
|
|
14730
|
+
`\u2713 minted '${r.name}' (${r.key_id}, env ${r.env}${r.end_user_required ? ", public / end_user_required" : ""}) with scopes [${r.scopes.join(", ")}]`,
|
|
14731
|
+
...r.end_user_required ? ["A public key belongs in your browser / mobile app: every request must carry the signed-in user's session (X-Vxil-End-User), and it acts only as that user."] : [],
|
|
14402
14732
|
`This key is shown ONCE and is not retrievable again \u2014 copy it now. ${storage} Revoke with \`vxil keys revoke ${r.key_id}\`.`,
|
|
14403
14733
|
...storeWarn
|
|
14404
14734
|
];
|
|
@@ -14904,7 +15234,7 @@ var VERB_USAGE = {
|
|
|
14904
15234
|
projects: "usage: vxil projects [list] [--json] \xB7 projects create <slug> [--name <display>] [--workload production|staging|development] [--json] \xB7 projects workload <slug> production|staging|development [--json] \xB7 projects rm <slug> [--yes] [--json]",
|
|
14905
15235
|
billing: "usage: vxil billing [status] [--json] \xB7 vxil billing upgrade --tier <free|developer|team|business> [--yes] [--json] (upgrade also downgrades: --tier free)",
|
|
14906
15236
|
files: "usage: vxil files put <path> --user <user_id> [--content-type <type>] [--json] \xB7 vxil files rm <object_id> [--yes] [--json]",
|
|
14907
|
-
webhooks: "usage: vxil webhooks sources add --provider <stripe|paddle|github|slack|revenuecat|generic> --name <n> [--forward-url <https>] \xB7 sources list \xB7 sources rm <source_id> [--yes] \xB7 sources test <source_id> \xB7 vxil webhooks events replay <event_id> \xB7 events runs <event_id>",
|
|
15237
|
+
webhooks: "usage: vxil webhooks sources add --provider <stripe|paddle|github|slack|revenuecat|generic|hmac> --name <n> [--forward-url <https> | --target-function <fn>] [--verify <json> (hmac)] \xB7 sources list \xB7 sources rm <source_id> [--yes] \xB7 sources test <source_id> \xB7 vxil webhooks events replay <event_id> \xB7 events runs <event_id>",
|
|
14908
15238
|
search: "usage: vxil search sync run <sourceKey> [--sweep] [--json] \xB7 vxil search sync reset <sourceKey> [--purge] [--yes] [--json]",
|
|
14909
15239
|
listen: "usage: vxil listen --forward-to <url> [--dev] [--source <id>] [--events <prefix,\u2026>] [--replay-last <n>] [--interval <s>] [--show-runs] [--raw] \u2014 forward inbound webhook events to a local server",
|
|
14910
15240
|
mcp: "usage: vxil mcp install [--client cursor|claude|vscode] [--project] [--print] [--key <api_key>] [--scopes a,b] [--name <server>]",
|
|
@@ -18054,12 +18384,13 @@ async function runListen(deps, opts) {
|
|
|
18054
18384
|
if (sres.body.error || sres.status >= 400) throw envelopeError(sres, "list webhook sources");
|
|
18055
18385
|
let tenantId = sres.body.meta?.tenant_id;
|
|
18056
18386
|
const sources = sres.body.data?.sources ?? [];
|
|
18387
|
+
const destOf = new Map(sources.map((s) => [s.source_id, s.target_function ? `fn:${s.target_function}` : s.forward_url || null]));
|
|
18057
18388
|
if (sources.length === 0) {
|
|
18058
18389
|
deps.err("no webhook sources yet \u2014 create one (dashboard \u2192 Webhooks, or POST /v1/webhooks/sources) and point the provider at its receiver URL; waiting for events anyway\u2026");
|
|
18059
18390
|
} else {
|
|
18060
18391
|
deps.err(`sources (${sources.length}):`);
|
|
18061
18392
|
for (const s of sources) {
|
|
18062
|
-
deps.err(` ${s.source_id} ${s.provider} ${s.name}${s.
|
|
18393
|
+
deps.err(` ${s.source_id} ${s.provider} ${s.name}${destOf.get(s.source_id)?.startsWith("fn:") ? ` \u2192 ${destOf.get(s.source_id)}` : ""}${destOf.get(s.source_id) ? "" : " (no forward_url \u2014 this listener is the only delivery)"}`);
|
|
18063
18394
|
}
|
|
18064
18395
|
}
|
|
18065
18396
|
let signingSecret;
|
|
@@ -18105,7 +18436,8 @@ async function runListen(deps, opts) {
|
|
|
18105
18436
|
} else {
|
|
18106
18437
|
const runs = rr.body.data?.runs ?? [];
|
|
18107
18438
|
if (runs.length === 0) {
|
|
18108
|
-
|
|
18439
|
+
const dest = ev.source_id ? destOf.get(ev.source_id) : void 0;
|
|
18440
|
+
deps.err(dest ? ` runs: none yet (the source delivers to ${dest})` : " runs: none (the source has no forward_url \u2014 this listener is the only delivery)");
|
|
18109
18441
|
} else {
|
|
18110
18442
|
for (const r of runs) {
|
|
18111
18443
|
deps.err(` run ${r.run_id}: ${r.state} (attempt ${r.attempt_number ?? 0}/${r.max_attempts ?? "?"}${r.dead_lettered ? ", DLQ" : ""})`);
|
|
@@ -22191,12 +22523,15 @@ try {
|
|
|
22191
22523
|
}
|
|
22192
22524
|
case "keys": {
|
|
22193
22525
|
const [sub, arg] = positional(0);
|
|
22194
|
-
const KEYS_USAGE = "usage: vxil keys mint --name <name> --scopes <a,b,c> [--env live|test] [--store] [--json] \xB7 vxil keys list [--json] \xB7 vxil keys revoke <key_id> [--yes] \xB7 vxil keys perms <key_id> [--allow <a,b> | --clear-allow] [--deny <c> | --clear-deny] [--json]";
|
|
22526
|
+
const KEYS_USAGE = "usage: vxil keys mint --name <name> --scopes <a,b,c> [--env live|test] [--class server|public] [--end-user-required] [--store] [--json] \xB7 vxil keys list [--json] \xB7 vxil keys revoke <key_id> [--yes] \xB7 vxil keys perms <key_id> [--allow <a,b> | --clear-allow] [--deny <c> | --clear-deny] [--json]";
|
|
22195
22527
|
if (sub === "help" || hasFlag("help")) {
|
|
22196
22528
|
console.log(`${KEYS_USAGE}
|
|
22197
22529
|
mint a NARROW scoped key for a server on the linked project \u2014 scopes are validated
|
|
22198
22530
|
locally (admin refused); the key prints ONCE on stdout (--json \u2192 { api_key, key_id,
|
|
22199
|
-
scopes }) and is stored as this machine's CLI key only under --store
|
|
22531
|
+
scopes }) and is stored as this machine's CLI key only under --store.
|
|
22532
|
+
--class public (or --end-user-required) mints the thin-client key for a browser /
|
|
22533
|
+
mobile app \u2014 the dashboard's "Public" class: it only works with a signed-in
|
|
22534
|
+
user's session, server-only scopes are refused, and it cannot be --store'd
|
|
22200
22535
|
list names, ids, scopes, env, created, last-used \u2014 never a secret; * marks the CLI's key
|
|
22201
22536
|
revoke type the key id to confirm (--yes for scripts); refuses the CLI's own key unless --yes
|
|
22202
22537
|
perms the key's MCP tool permissions: with no flag, shows them; --allow / --deny set a
|
|
@@ -22226,7 +22561,14 @@ try {
|
|
|
22226
22561
|
if ("error" in scopes) fail(scopes.error);
|
|
22227
22562
|
const env = parseKeyEnv(flag("env"));
|
|
22228
22563
|
if ("error" in env) fail(env.error);
|
|
22229
|
-
|
|
22564
|
+
const cls = parseKeyClass(flag("class"), hasFlag("end-user-required"));
|
|
22565
|
+
if ("error" in cls) fail(cls.error);
|
|
22566
|
+
if (cls.keyClass === "public") {
|
|
22567
|
+
const bad = checkPublicKeyScopes(scopes.scopes);
|
|
22568
|
+
if (bad) fail(bad.error);
|
|
22569
|
+
if (hasFlag("store")) fail("--store refused for a public key \u2014 it cannot drive the CLI (every request needs an end-user session); copy it into your app instead");
|
|
22570
|
+
}
|
|
22571
|
+
return { name: name.name, scopes: scopes.scopes, env: env.env, keyClass: cls.keyClass };
|
|
22230
22572
|
})() : void 0;
|
|
22231
22573
|
const t = resolveTargetOrFail(selector());
|
|
22232
22574
|
if (!t.tenantSlug) fail("no linked project \u2014 run `vxil link <slug>` (or `vxil quickstart`) first; keys are minted on the bound project");
|
|
@@ -22640,10 +22982,16 @@ try {
|
|
|
22640
22982
|
}
|
|
22641
22983
|
case "webhooks": {
|
|
22642
22984
|
const [sub, verb, arg] = positional(0);
|
|
22643
|
-
const WEBHOOKS_USAGE = "usage: vxil webhooks sources add --provider <stripe|paddle|github|slack|revenuecat|generic> --name <n> [--forward-url <https>] \xB7 sources list \xB7 sources rm <source_id> [--yes] \xB7 sources test <source_id> \xB7 vxil webhooks events replay <event_id> \xB7 events runs <event_id>";
|
|
22985
|
+
const WEBHOOKS_USAGE = "usage: vxil webhooks sources add --provider <stripe|paddle|github|slack|revenuecat|generic|hmac> --name <n> [--forward-url <https> | --target-function <fn>] [--verify <json> (hmac)] \xB7 sources list \xB7 sources rm <source_id> [--yes] \xB7 sources test <source_id> \xB7 vxil webhooks events replay <event_id> \xB7 events runs <event_id>";
|
|
22644
22986
|
if (sub === "sources") {
|
|
22645
22987
|
if (verb === "add") {
|
|
22646
|
-
const p = parseSourceAdd({
|
|
22988
|
+
const p = parseSourceAdd({
|
|
22989
|
+
provider: flag("provider"),
|
|
22990
|
+
name: flag("name"),
|
|
22991
|
+
forwardUrl: flag("forward-url"),
|
|
22992
|
+
targetFunction: flag("target-function"),
|
|
22993
|
+
verify: flag("verify")
|
|
22994
|
+
});
|
|
22647
22995
|
if ("error" in p) fail(p.error);
|
|
22648
22996
|
const { api, baseUrl } = requireApi(selector(), { write: `webhooks sources add ${p.body.name}` });
|
|
22649
22997
|
const s = await sourcesAdd(api, p.body);
|
|
@@ -22656,7 +23004,10 @@ try {
|
|
|
22656
23004
|
const rows = await sourcesList(api);
|
|
22657
23005
|
if (jsonOut) console.log(JSON.stringify({ sources: rows }, null, 2));
|
|
22658
23006
|
else if (!rows.length) console.log("(no inbound sources \u2014 `vxil webhooks sources add --provider \u2026 --name \u2026`)");
|
|
22659
|
-
else for (const r of rows)
|
|
23007
|
+
else for (const r of rows) {
|
|
23008
|
+
const dest = sourceDestination(r);
|
|
23009
|
+
console.log(`${r.source_id} ${String(r.provider).padEnd(10)} ${r.name}${dest ? ` \u2192 ${dest}` : ""}`);
|
|
23010
|
+
}
|
|
22660
23011
|
break;
|
|
22661
23012
|
}
|
|
22662
23013
|
if (verb === "rm") {
|
|
@@ -23398,7 +23749,7 @@ export default defineConfig(${JSON.stringify({ features: featBlocks, cms: { coll
|
|
|
23398
23749
|
default:
|
|
23399
23750
|
console.log(`vxil \u2014 a Supabase-grade code-first CLI for the whole platform
|
|
23400
23751
|
--version \xB7 init [--template <id>] \xB7 templates \xB7 try (keyless sandbox) \xB7 quickstart [--features a,b] [--invite <code>] [--env <label>] [--dev [--ttl <h>]] [--no-push] \xB7 login \xB7 link <slug> [--key <k> [--tenant <id>]] [--as dev|<name>] [--env <label>] [--mint]
|
|
23401
|
-
keys mint --name <n> --scopes <a,b,c> [--env live|test] [--store] \xB7 keys list \xB7 keys revoke <key_id> [--yes] \u2014 a NARROW scoped key for a server (shown once; needs a vxil login session)
|
|
23752
|
+
keys mint --name <n> --scopes <a,b,c> [--env live|test] [--class server|public] [--store] \xB7 keys list \xB7 keys revoke <key_id> [--yes] \u2014 a NARROW scoped key for a server (shown once; needs a vxil login session)
|
|
23402
23753
|
keys perms <key_id> [--allow <a,b> | --clear-allow] [--deny <c> | --clear-deny] \u2014 a key's MCP tool permissions (admin+)
|
|
23403
23754
|
account [show] \xB7 account set [--locale <code>] [--display-name <n>] \xB7 account password \xB7 logout \xB7 invites [list] \xB7 invites generate [--count <n>] \xB7 invites accept <token|url>
|
|
23404
23755
|
members [list] \xB7 members invite <email> --role <r> \xB7 members add <email> --role <r> \xB7 members rm <user_id|email> [--yes] \xB7 members uninvite <email|id> [--yes] \u2014 the bound project's team (session)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vxil/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The vxil CLI — init, quickstart, push, gen, secrets, keys, migrate, doctor, diff, functions, payments; installs the `vxil` command (npm i -g @vxil/cli).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"miniflare": "^4.20260611.0",
|
|
49
|
-
"@vxil/config": "0.
|
|
49
|
+
"@vxil/config": "0.13.0",
|
|
50
|
+
"@vxil/feature-configs": "0.11.0",
|
|
50
51
|
"@vxil/runtime": "0.0.1",
|
|
51
|
-
"@vxil/types": "0.0.1"
|
|
52
|
-
"@vxil/feature-configs": "0.10.0"
|
|
52
|
+
"@vxil/types": "0.0.1"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "pnpm --filter @vxil/feature-configs run build && pnpm --filter @vxil/config run build && esbuild bin/vxil.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/vxil.js --banner:js='#!/usr/bin/env node' --external:esbuild --external:miniflare && esbuild src/config-entry.ts --bundle --platform=node --format=esm --target=node24 --outfile=dist/config.js && pnpm exec tsx scripts/build-config-dts.ts",
|