@vxil/cli 0.32.0 → 0.35.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.
@@ -0,0 +1,16 @@
1
+ /** ALERT-CHAT-DEST: the secret-name grammar of `webhooks.alerts.chatWebhook.secret`
2
+ * (the secret store's own SECRET_NAME_RE, optionally `secret:`-prefixed like
3
+ * every other *Ref). */
4
+ export declare const CHAT_WEBHOOK_SECRET_PATTERN = "^(secret:)?[a-z0-9_]{1,64}$";
5
+ /** ALERT-CHAT-DEST: where a failure-alert chat webhook may point. EXACT hosts,
6
+ * plus two Microsoft Teams Workflows suffixes (each tenant gets its own
7
+ * sub-domain there). Nothing else: the digest cron POSTs from vxil's own
8
+ * network, so an open URL would be an SSRF / exfiltration door. */
9
+ export declare const CHAT_WEBHOOK_HOSTS: readonly string[];
10
+ export declare const CHAT_WEBHOOK_HOST_SUFFIXES: readonly string[];
11
+ /** ALERT-CHAT-DEST: null when `raw` is a chat webhook URL the digest may POST
12
+ * to, else a short reason. https only, default port, no credentials in the
13
+ * authority, host on the allowlist. PURE — shared by the digest cron (the
14
+ * enforcing check) and the dashboard (an early hint before the value is
15
+ * stored write-only). */
16
+ export declare function chatWebhookUrlError(raw: string): string | null;
@@ -0,0 +1,83 @@
1
+ import { type Static } from '@sinclair/typebox';
2
+ /** = control-plane appgenTypes RULE_KINDS (a CI test pins the two equal). */
3
+ export declare const CMS_RULE_KINDS: readonly ["stamp_actor", "create_by_role", "owner_only_edit", "role_gate", "archive_by_role", "state_flow", "no_self_decision", "capacity", "no_overlap"];
4
+ export type CmsRuleKind = typeof CMS_RULE_KINDS[number];
5
+ /** Entries in one manifest (an app compiles ≤ a few dozen; the bag stays small). */
6
+ export declare const CMS_RULES_MAX = 64;
7
+ /** = control-plane appgenRules MAX_LIMIT (a capacity limit is 1..this). */
8
+ export declare const CMS_RULE_MAX_LIMIT = 100000;
9
+ /** An App builder app id (`app_` + a 26-char Crockford ULID, any case). */
10
+ export declare const CMS_RULE_APP_PATTERN = "^app_[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$";
11
+ /** One rule's compiled parameters (= control-plane appgenTypes RuleParams). */
12
+ export declare const CmsRuleParamsSchema: import("@sinclair/typebox").TObject<{
13
+ field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
14
+ actor: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
15
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>>>;
16
+ transitions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
17
+ from: import("@sinclair/typebox").TString;
18
+ to: import("@sinclair/typebox").TString;
19
+ role: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>, import("@sinclair/typebox").TLiteral<"actor">]>>;
20
+ }>>>;
21
+ initial: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
22
+ key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
23
+ parent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
24
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
25
+ parent_field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
26
+ start: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
27
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
28
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
29
+ live: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
30
+ }>;
31
+ export declare const CmsRuleEntrySchema: import("@sinclair/typebox").TObject<{
32
+ app: import("@sinclair/typebox").TString;
33
+ kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"owner_only_edit" | "archive_by_role" | "state_flow" | "stamp_actor" | "create_by_role" | "role_gate" | "no_self_decision" | "capacity" | "no_overlap">[]>;
34
+ collection: import("@sinclair/typebox").TString;
35
+ params: import("@sinclair/typebox").TObject<{
36
+ field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
37
+ actor: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
38
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>>>;
39
+ transitions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
40
+ from: import("@sinclair/typebox").TString;
41
+ to: import("@sinclair/typebox").TString;
42
+ role: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>, import("@sinclair/typebox").TLiteral<"actor">]>>;
43
+ }>>>;
44
+ initial: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
45
+ key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
46
+ parent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
47
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
48
+ parent_field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
49
+ start: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
50
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
51
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
52
+ live: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
53
+ }>;
54
+ }>;
55
+ export type CmsRuleEntry = Static<typeof CmsRuleEntrySchema>;
56
+ export type CmsRuleParams = Static<typeof CmsRuleParamsSchema>;
57
+ /** THE canonical rule key: shared by the server diff, the CLI render and
58
+ * the backfill. A param change is a different key
59
+ * (a removal plus an addition). The app id is compared lowercased. */
60
+ export declare function cmsRuleKey(e: Pick<CmsRuleEntry, 'app' | 'kind' | 'collection' | 'params'>): string;
61
+ /** The lists and fields one entry reads (its own list, and a capacity
62
+ * limit's parent list + field). */
63
+ export declare function cmsRuleReads(e: Pick<CmsRuleEntry, 'collection' | 'params'>): Array<{
64
+ collection: string;
65
+ fields: string[];
66
+ }>;
67
+ /** The stored entries a schema change on `collection` reads: `field` null =
68
+ * a list-level change (policy, owner, access, roles, drop) — every entry on
69
+ * the list or naming it as a parent; else only the entries reading that field. */
70
+ export declare function cmsRulesReading(rules: unknown, collection: string, field: string | null): CmsRuleEntry[];
71
+ /** One owner-facing English sentence for an entry (the CLI pull comment, the
72
+ * rules_widening / destructive-ack messages). Built from the closed kind and
73
+ * its validated identifiers only. */
74
+ export declare function cmsRuleSentence(e: Pick<CmsRuleEntry, 'kind' | 'collection' | 'params'>): string;
75
+ /** The fields-route meta-ops that change who may read or write a list (its
76
+ * sharing policy): on a list a rule reads they need the destructive ack. */
77
+ export declare const CMS_RULE_LIST_META_KEYS: readonly ["owner_field", "end_user_access", "write_roles", "org_field", "org_read_roles"];
78
+ /** The stored entries a cms fields-route body touches on
79
+ * `collection` — a policy meta-op (any entry on the list or naming it as a
80
+ * parent), a field definition or a `drop_field` naming a field a rule reads.
81
+ * Other bodies (a new unrelated field, retention, actions, public) touch none.
82
+ * Shared by cms-v1 (the 422 ack) and /v1/apply (the plan holds the step). */
83
+ export declare function cmsRulesTouchedByFieldBody(rules: unknown, collection: string, body: unknown): CmsRuleEntry[];
@@ -224,4 +224,24 @@ export declare function highlightTokens(src: string): Array<{
224
224
  value: string;
225
225
  kind: HlKind;
226
226
  }>;
227
+ /** = control-plane `appHookOwnership.APP_HOOK_RE` (the App builder ownership
228
+ * marker `app_` + an app id + `_`; parity pinned by the control-plane
229
+ * appgenDoors test). Only a name carrying a real App builder app id. */
230
+ export declare const APP_OWNED_HOOK_RE: RegExp;
231
+ /** The App builder rule kinds whose DELETE is not free for an end user
232
+ * (= the non-'free' keys of control-plane `appgenDoors.DELETE_SEMANTICS`;
233
+ * parity pinned by the control-plane appgenDoors test). appgenRules names a
234
+ * rule's hooks `<stem><collection>_<kind>[…]`. */
235
+ export declare const APP_DELETE_GUARDED_RULE_KINDS: readonly ["owner_only_edit", "archive_by_role", "state_flow"];
236
+ /** The collections that carry an enabled App builder hook of a
237
+ * delete-guarded rule kind. An END-USER cascade (cascade / set_null) into one
238
+ * of them is refused by cms runCascade — the platform twin of the app proxy's
239
+ * D5 parent-delete refusal (a guard on the child list cannot be bypassed by
240
+ * deleting its parent). Structural: the hook NAME the App builder compiler
241
+ * writes, never user text. A rule that compiles no write check (archive_by_role
242
+ * on the proxy lane) carries the always-true `<list>_archive_by_role_delete_guard`
243
+ * marker (control-plane appgenRules.withDeleteGuardMarkers), so this set equals
244
+ * the lists the proxy's appgenDoors.doorsOf guards (parity pinned in the
245
+ * control-plane appgenDoors test). Pure. */
246
+ export declare function appDeleteGuardedCollections(hooks: Record<string, HookDef> | undefined): ReadonlySet<string>;
227
247
  export {};
@@ -81,7 +81,7 @@ export declare function validateCdcConfig(cdc: Record<string, CdcRuleLike> | und
81
81
  * `orgField`: the workspace-scope field (null/absent = none). */
82
82
  export interface CdcCollectionGates {
83
83
  readRoleFields?: readonly string[];
84
- endUserAccess?: 'readwrite' | 'read' | 'none';
84
+ endUserAccess?: 'readwrite' | 'read' | 'none' | 'read_all';
85
85
  ownerField?: string | null;
86
86
  orgField?: string | null;
87
87
  }
@@ -1,11 +1,13 @@
1
1
  import { type Static, type TSchema } from '@sinclair/typebox';
2
2
  import { ACTIVITY_FEED_BUILTIN_GROUPS } from './_vxil-feature-configs-feedGroups.js';
3
+ export { CHAT_WEBHOOK_HOSTS, CHAT_WEBHOOK_HOST_SUFFIXES, CHAT_WEBHOOK_SECRET_PATTERN, chatWebhookUrlError } from './_vxil-feature-configs-chatWebhook.js';
3
4
  export { AI_THINKING_LEVELS, geminiThinkingLevelError, minimalThinkingLevel, type GeminiThinkingLevel } from './_vxil-feature-configs-geminiThinking.js';
4
5
  export * from './_vxil-feature-configs-hooks.js';
5
6
  export * from './_vxil-feature-configs-readmodels.js';
6
7
  export * from './_vxil-feature-configs-apiState.js';
7
8
  export * from './_vxil-feature-configs-canonicalJson.js';
8
9
  export * from './_vxil-feature-configs-publicAssets.js';
10
+ export * from './_vxil-feature-configs-cmsRules.js';
9
11
  export declare const RESERVED_CREDIT_TYPES: ReadonlySet<string>;
10
12
  /** A function binding's `retry.maxAttempts` ceiling, and the
11
13
  * binding kinds that may carry `retry` — every lane the platform delivers
@@ -225,7 +227,7 @@ export type JobsConfig = Static<typeof JobsConfigSchema>;
225
227
  declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
226
228
  issuer: import("@sinclair/typebox").TString;
227
229
  clientId: import("@sinclair/typebox").TString;
228
- clientSecretRef: import("@sinclair/typebox").TString;
230
+ clientSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
229
231
  scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
230
232
  claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
231
233
  email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -284,7 +286,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
284
286
  oidc: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
285
287
  issuer: import("@sinclair/typebox").TString;
286
288
  clientId: import("@sinclair/typebox").TString;
287
- clientSecretRef: import("@sinclair/typebox").TString;
289
+ clientSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
288
290
  scopes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
289
291
  claims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
290
292
  email: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -439,6 +441,9 @@ export declare const WebhooksConfigSchema: import("@sinclair/typebox").TObject<{
439
441
  enabled: import("@sinclair/typebox").TBoolean;
440
442
  minLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"warn">, import("@sinclair/typebox").TLiteral<"error">]>;
441
443
  digestMinutes: import("@sinclair/typebox").TInteger;
444
+ chatWebhook: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
445
+ secret: import("@sinclair/typebox").TString;
446
+ }>>;
442
447
  }>>;
443
448
  subscriptions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
444
449
  target_url: import("@sinclair/typebox").TString;
@@ -454,6 +459,15 @@ export declare const CommentsConfigSchema: import("@sinclair/typebox").TObject<{
454
459
  editWindowMinutes: import("@sinclair/typebox").TInteger;
455
460
  }>;
456
461
  export type CommentsConfig = Static<typeof CommentsConfigSchema>;
462
+ /** The most collections a project's cms may be configured for (the schema
463
+ * maximum of `limits.maxCollections`); at it, "raise Max collections" is no
464
+ * longer an answer. */
465
+ export declare const CMS_MAX_COLLECTIONS_CEILING = 200;
466
+ /** The cms query page-size defaults (the schema defaults; cmsQueryConfig's fallback). */
467
+ export declare const CMS_QUERY_DEFAULTS: {
468
+ readonly maxPageSize: 100;
469
+ readonly defaultPageSize: 25;
470
+ };
457
471
  export declare const CmsConfigSchema: import("@sinclair/typebox").TObject<{
458
472
  enabled: import("@sinclair/typebox").TBoolean;
459
473
  draftPublish: import("@sinclair/typebox").TBoolean;
@@ -464,10 +478,10 @@ export declare const CmsConfigSchema: import("@sinclair/typebox").TObject<{
464
478
  maxItemBytes: import("@sinclair/typebox").TInteger;
465
479
  maxItemsPerCollection: import("@sinclair/typebox").TInteger;
466
480
  }>;
467
- query: import("@sinclair/typebox").TObject<{
481
+ query: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
468
482
  maxPageSize: import("@sinclair/typebox").TInteger;
469
483
  defaultPageSize: import("@sinclair/typebox").TInteger;
470
- }>;
484
+ }>>;
471
485
  relations: import("@sinclair/typebox").TObject<{
472
486
  maxExpandDepth: import("@sinclair/typebox").TInteger;
473
487
  maxExpandFields: import("@sinclair/typebox").TInteger;
@@ -499,8 +513,43 @@ export declare const CmsConfigSchema: import("@sinclair/typebox").TObject<{
499
513
  payload: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ids">, import("@sinclair/typebox").TLiteral<"full">]>>;
500
514
  enabled: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
501
515
  }>>>;
516
+ rules: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
517
+ app: import("@sinclair/typebox").TString;
518
+ kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"owner_only_edit" | "archive_by_role" | "state_flow" | "stamp_actor" | "create_by_role" | "role_gate" | "no_self_decision" | "capacity" | "no_overlap">[]>;
519
+ collection: import("@sinclair/typebox").TString;
520
+ params: import("@sinclair/typebox").TObject<{
521
+ field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
522
+ actor: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
523
+ roles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>>>;
524
+ transitions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
525
+ from: import("@sinclair/typebox").TString;
526
+ to: import("@sinclair/typebox").TString;
527
+ role: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"owner">, import("@sinclair/typebox").TLiteral<"admin">, import("@sinclair/typebox").TLiteral<"member">]>, import("@sinclair/typebox").TLiteral<"actor">]>>;
528
+ }>>>;
529
+ initial: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
530
+ key: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
531
+ parent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
532
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
533
+ parent_field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
534
+ start: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
535
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
536
+ status: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
537
+ live: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
538
+ }>;
539
+ }>>>;
502
540
  }>;
503
541
  export type CmsConfig = Static<typeof CmsConfigSchema>;
542
+ /** The resolved cms query page sizes (the bag is Optional in the type;
543
+ * every persisted manifest still carries it). */
544
+ export declare function cmsQueryConfig(config: {
545
+ query?: {
546
+ maxPageSize?: number;
547
+ defaultPageSize?: number;
548
+ } | undefined;
549
+ } | null | undefined): {
550
+ maxPageSize: number;
551
+ defaultPageSize: number;
552
+ };
504
553
  export declare const McpConfigSchema: import("@sinclair/typebox").TObject<{
505
554
  enabled: import("@sinclair/typebox").TBoolean;
506
555
  exposureLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"all">, import("@sinclair/typebox").TLiteral<"read-only">, import("@sinclair/typebox").TLiteral<"custom">]>;
package/dist/config.d.ts CHANGED
@@ -114,13 +114,17 @@ export interface CollectionDef {
114
114
  * filtered delete, batch write — and a delete elsewhere
115
115
  * whose on_delete cascade / set_null would change a row
116
116
  * here) is `403 server_only`;
117
- * 'none' — no end-user reads or writes at all, even with ownerField.
117
+ * 'none' — no end-user reads or writes at all, even with ownerField;
118
+ * 'read_all' — every verified end user reads every row (past the
119
+ * ownerField predicate); writes stay owner-scoped as
120
+ * 'readwrite' (a members board: everyone reads, each edits
121
+ * only their own).
118
122
  * Server keys (and platform writes such as a jobs status mirror) are never
119
123
  * affected. Stored on the collection (model data, not a config leaf) and
120
- * carried by BOTH push paths; widening ('read'/'none' → 'readwrite') needs
124
+ * carried by BOTH push paths; widening (towards 'readwrite', then 'read_all') needs
121
125
  * `vxil push --allow-destructive`. Any other value fails `vxil plan` /
122
126
  * `vxil push` (it never falls back to 'readwrite'). */
123
- endUserAccess?: 'readwrite' | 'read' | 'none';
127
+ endUserAccess?: 'readwrite' | 'read' | 'none' | 'read_all';
124
128
  /** Role slugs one of which a VERIFIED end user must hold to WRITE this
125
129
  * collection (create, update, $inc, delete, publish, transaction step,
126
130
  * filtered delete, batch write, and an end-user delete's cascade into it) —
@@ -132,6 +136,15 @@ export interface CollectionDef {
132
136
  * a config leaf) and carried by BOTH push paths; widening (clearing, or
133
137
  * adding a role) needs `vxil push --allow-destructive`. */
134
138
  writeRoles?: string[];
139
+ /** W39-3: role slugs whose VERIFIED holders MANAGE this owned collection —
140
+ * they read every row (get / list / count) and may update another member's
141
+ * row through the single-item PATCH; the collection's hooks still decide
142
+ * which fields (every other door stays owner-scoped). Needs `ownerField`
143
+ * and an end-user-writable `endUserAccess`. Absent / `[]` = none. Up to 16
144
+ * slugs. Carried by BOTH push paths; adding a role widens reads and needs
145
+ * `vxil push --allow-destructive`. The App builder sets `['app_manager']`
146
+ * on its members apps' own-only lists while the app names managers. */
147
+ managerRoles?: string[];
135
148
  /** Retention in days (guide ch. 4): items created more than this many days
136
149
  * ago are soft-deleted by the nightly sweep (recoverable for 30 days). An
137
150
  * integer 1..3650, or `null` to keep items until deleted. ABSENT = not