@zveltio/sdk 3.0.0-beta.76 → 3.0.0-beta.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -186,18 +186,18 @@ await sync.stop();
186
186
  The engine exposes two endpoints for batch sync:
187
187
 
188
188
  - `POST /api/sync/push` — send offline operations to server
189
- - `POST /api/sync/pull` — pull changes since a timestamp
189
+ - `POST /api/sync/pull` — pull changes after your cursors
190
190
 
191
191
  `SyncManager.pull()` uses `/api/sync/pull` and keeps its cursors in the local store. Both endpoints take a
192
192
  session only: a client authenticated by an API key gets 403, and `pull()` then falls back to paging the
193
193
  collections API, which cannot report deletes.
194
194
 
195
- A pull returns at most 1000 rows per collection. Send `{ collections, since, cursors }`, merge the
195
+ A pull returns at most 1000 rows per collection. Send `{ collections, cursors }`, merge the
196
196
  response's `cursors` into yours, and pull again while `hasMore` is true. Keep `cursors` for the next
197
- sync — a collection's cursor replaces `since` for it, and is the only position that pages through rows
198
- sharing one `updated_at` (a bulk import).
197
+ sync; a collection without one is read from its start. Cursors are opaque: there is no timestamp
198
+ position, because a timestamp cannot page through rows sharing one `updated_at` (a bulk import).
199
199
 
200
200
  A row deleted on the server comes back as `{ operation: 'delete', data: null }`; drop your copy. Deletes are
201
- kept for 30 days: a cursor older than that gets `resync[collection] === true`, and the collection restarts
201
+ kept for 30 days: a cursor last caught up longer ago than that gets `resync[collection] === true`, and the collection restarts
202
202
  from the first row — drop your synced copies of it before applying the page. A first pull (no cursor) gets
203
203
  no deletes, so start it from an empty copy.
@@ -84,6 +84,17 @@ export interface CollectionDefinition {
84
84
  isSystem?: boolean;
85
85
  schemaLocked?: boolean;
86
86
  }
87
+ /** A row from `zvd_relations`, as `getRelations` returns it. */
88
+ export interface RelationRecord {
89
+ id: string;
90
+ name: string;
91
+ type: string;
92
+ source_collection: string;
93
+ source_field: string;
94
+ target_collection: string;
95
+ target_field: string | null;
96
+ junction_table: string | null;
97
+ }
87
98
  /** A row from `zv_collections` / `getCollection` / `getCollections`. The
88
99
  * exact shape is engine-internal; this interface only fixes the fields
89
100
  * extensions are documented to read. Treat unknown keys as best-effort. */
@@ -135,6 +146,8 @@ export interface DDLManager {
135
146
  removeField(db: DDLManagerDb, collectionName: string, fieldName: string): Promise<void>;
136
147
  /** List every collection registered in `zv_collections`. */
137
148
  getCollections(db: DDLManagerDb): Promise<CollectionRecord[]>;
149
+ /** Every registered relation, or only those whose source or target is `collection`. */
150
+ getRelations(db: DDLManagerDb, collection?: string): Promise<RelationRecord[]>;
138
151
  /** Read a single collection by name. Returns `null` when missing. */
139
152
  getCollection(db: DDLManagerDb, name: string): Promise<CollectionRecord | null>;
140
153
  /** Update display-only metadata (label, description, icon, sort, etc.). */
@@ -142,6 +155,9 @@ export interface DDLManager {
142
155
  /** Introspect the live PG table and return its field configs. Useful for
143
156
  * extensions that auto-discover schema changes. */
144
157
  introspectTable(db: DDLManagerDb, collectionName: string): Promise<FieldDefinition[]>;
158
+ /** Every physical column of the collection's table, system columns included,
159
+ * in table order; `[]` when the table does not exist. */
160
+ columnNames(db: DDLManagerDb, collectionName: string): Promise<string[]>;
145
161
  /** Re-read PG and write the result back to `zv_collections.fields`. Returns
146
162
  * the number of changes detected. */
147
163
  syncFieldsFromDB(db: DDLManagerDb, collectionName: string): Promise<number>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ddl/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ddl/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Capabilities as TYPES — roadmap R8: "an undeclared capability is a compile
3
+ * error, not a runtime refusal".
4
+ *
5
+ * The engine refuses a gated `ctx.internals` member whose capability the
6
+ * manifest does not declare (`CapabilityDeniedError`). That refusal happens the
7
+ * first time the code path runs — often in production, on the request that
8
+ * needed it. Declaring the capabilities to TypeScript as well moves the same
9
+ * answer to the editor:
10
+ *
11
+ * export default defineExtension(['ddl', 'secrets'], {
12
+ * name: 'my-ext',
13
+ * category: 'tools',
14
+ * async register(app, ctx) {
15
+ * await ctx.internals.encryptSecret('x'); // ok: 'secrets'
16
+ * await ctx.internals.deleteUser(db, 'u'); // compile error: needs 'auth:users'
17
+ * },
18
+ * });
19
+ *
20
+ * The list passed to `defineExtension` is a statement to the compiler; the
21
+ * manifest's `permissions` remains what the engine enforces and what an
22
+ * administrator approves. `zveltio extension validate` compares the two.
23
+ *
24
+ * These two tables mirror the engine's `CAPABILITIES` and `INTERNALS_CAPABILITY`
25
+ * (`packages/engine/src/lib/extensions/capabilities.ts`); the engine's unit test
26
+ * `sdk-capability-types.test.ts` fails when they drift apart.
27
+ */
28
+ /** Every capability an extension may declare, `net:<host>` aside. */
29
+ export declare const EXTENSION_CAPABILITIES: readonly ["db:admin", "tenant:enter", "ddl", "secrets", "auth:session", "auth:users", "identity:provision", "audit:read", "data:write", "data:system", "notifications", "files", "documents", "edge-functions", "introspection", "storage", "cron", "field-types"];
30
+ /** A capability name, including the parameterised `net:<host>`. */
31
+ export type Capability = (typeof EXTENSION_CAPABILITIES)[number] | `net:${string}`;
32
+ /** The `ctx.internals` members a capability unlocks. */
33
+ export declare const CAPABILITY_MEMBERS: {
34
+ readonly enqueueDDLJob: "ddl";
35
+ readonly introspectSchema: "introspection";
36
+ readonly extensionRegistry: "introspection";
37
+ readonly encryptSecret: "secrets";
38
+ readonly decryptSecret: "secrets";
39
+ readonly deriveTokenHash: "secrets";
40
+ readonly createBetterAuthSession: "auth:session";
41
+ readonly deleteUser: "auth:users";
42
+ readonly exportUserData: "auth:users";
43
+ readonly revokeUserSessions: "auth:users";
44
+ readonly setUserActive: "auth:users";
45
+ readonly liftOwnBan: "auth:users";
46
+ readonly provisionUser: "identity:provision";
47
+ readonly listTenantUsers: "identity:provision";
48
+ readonly updateUserProfile: "identity:provision";
49
+ readonly addTenantMember: "identity:provision";
50
+ readonly removeTenantMember: "identity:provision";
51
+ readonly setTenantMembershipEnd: "identity:provision";
52
+ readonly readAuditActivity: "audit:read";
53
+ readonly createRecord: "data:write";
54
+ readonly updateRecord: "data:write";
55
+ readonly deleteRecord: "data:write";
56
+ readonly asSystem: "data:system";
57
+ readonly sendNotification: "notifications";
58
+ readonly moveToTrash: "files";
59
+ readonly generatePDFAsync: "documents";
60
+ readonly runEdgeFunction: "edge-functions";
61
+ };
62
+ /** Members of `ctx.internals` that need a capability. */
63
+ export type GatedMember = keyof typeof CAPABILITY_MEMBERS;
64
+ /** The gated members the capabilities `C` unlock. */
65
+ export type MembersUnlockedBy<C extends string> = {
66
+ [K in GatedMember]: (typeof CAPABILITY_MEMBERS)[K] extends C ? K : never;
67
+ }[GatedMember];
68
+ /** The gated members `C` does NOT unlock — removed from the narrowed type. */
69
+ export type MembersLockedFor<C extends string> = Exclude<GatedMember, MembersUnlockedBy<C>>;
70
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/extension/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,oQAmBzB,CAAC;AAEX,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,MAAM,EAAE,CAAC;AAEnF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4B6C,CAAC;AAE7E,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAE1D,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,IAAI;KAC/C,CAAC,IAAI,WAAW,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,KAAK;CACzE,CAAC,WAAW,CAAC,CAAC;AAEf,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,IAAI,OAAO,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Capabilities as TYPES — roadmap R8: "an undeclared capability is a compile
3
+ * error, not a runtime refusal".
4
+ *
5
+ * The engine refuses a gated `ctx.internals` member whose capability the
6
+ * manifest does not declare (`CapabilityDeniedError`). That refusal happens the
7
+ * first time the code path runs — often in production, on the request that
8
+ * needed it. Declaring the capabilities to TypeScript as well moves the same
9
+ * answer to the editor:
10
+ *
11
+ * export default defineExtension(['ddl', 'secrets'], {
12
+ * name: 'my-ext',
13
+ * category: 'tools',
14
+ * async register(app, ctx) {
15
+ * await ctx.internals.encryptSecret('x'); // ok: 'secrets'
16
+ * await ctx.internals.deleteUser(db, 'u'); // compile error: needs 'auth:users'
17
+ * },
18
+ * });
19
+ *
20
+ * The list passed to `defineExtension` is a statement to the compiler; the
21
+ * manifest's `permissions` remains what the engine enforces and what an
22
+ * administrator approves. `zveltio extension validate` compares the two.
23
+ *
24
+ * These two tables mirror the engine's `CAPABILITIES` and `INTERNALS_CAPABILITY`
25
+ * (`packages/engine/src/lib/extensions/capabilities.ts`); the engine's unit test
26
+ * `sdk-capability-types.test.ts` fails when they drift apart.
27
+ */
28
+ /** Every capability an extension may declare, `net:<host>` aside. */
29
+ export const EXTENSION_CAPABILITIES = [
30
+ 'db:admin',
31
+ 'tenant:enter',
32
+ 'ddl',
33
+ 'secrets',
34
+ 'auth:session',
35
+ 'auth:users',
36
+ 'identity:provision',
37
+ 'audit:read',
38
+ 'data:write',
39
+ 'data:system',
40
+ 'notifications',
41
+ 'files',
42
+ 'documents',
43
+ 'edge-functions',
44
+ 'introspection',
45
+ 'storage',
46
+ 'cron',
47
+ 'field-types',
48
+ ];
49
+ /** The `ctx.internals` members a capability unlocks. */
50
+ export const CAPABILITY_MEMBERS = {
51
+ enqueueDDLJob: 'ddl',
52
+ introspectSchema: 'introspection',
53
+ extensionRegistry: 'introspection',
54
+ encryptSecret: 'secrets',
55
+ decryptSecret: 'secrets',
56
+ deriveTokenHash: 'secrets',
57
+ createBetterAuthSession: 'auth:session',
58
+ deleteUser: 'auth:users',
59
+ exportUserData: 'auth:users',
60
+ revokeUserSessions: 'auth:users',
61
+ setUserActive: 'auth:users',
62
+ liftOwnBan: 'auth:users',
63
+ provisionUser: 'identity:provision',
64
+ listTenantUsers: 'identity:provision',
65
+ updateUserProfile: 'identity:provision',
66
+ addTenantMember: 'identity:provision',
67
+ removeTenantMember: 'identity:provision',
68
+ setTenantMembershipEnd: 'identity:provision',
69
+ readAuditActivity: 'audit:read',
70
+ createRecord: 'data:write',
71
+ updateRecord: 'data:write',
72
+ deleteRecord: 'data:write',
73
+ asSystem: 'data:system',
74
+ sendNotification: 'notifications',
75
+ moveToTrash: 'files',
76
+ generatePDFAsync: 'documents',
77
+ runEdgeFunction: 'edge-functions',
78
+ };
@@ -1,3 +1,4 @@
1
+ import type { Capability, MembersLockedFor } from './capabilities.js';
1
2
  import type { Context, Hono } from 'hono';
2
3
  import type { Kysely, RawBuilder } from 'kysely';
3
4
  export { permissionGate } from './permission-gate.js';
@@ -164,7 +165,14 @@ export interface ExtensionConfig {
164
165
  * Default `any` keeps legacy extensions compiling untouched. Migrated
165
166
  * extensions get full Kysely autocomplete + typo detection.
166
167
  */
167
- export interface ExtensionContext<DB = any> {
168
+ /**
169
+ * `C` — the capabilities this extension declared, when it says so through
170
+ * `defineExtension`. It narrows `internals` to the members those capabilities
171
+ * unlock and `adminDb` to `db:admin`, so an undeclared capability is a compile
172
+ * error rather than a `CapabilityDeniedError` on the first request that needs
173
+ * it. Defaults to every capability: code that does not opt in keeps today's type.
174
+ */
175
+ export interface ExtensionContext<DB = any, C extends string = Capability> {
168
176
  /**
169
177
  * Kysely database instance (restricted — cannot query zv_* system tables).
170
178
  * Typed via the `DB` generic; use `ZveltioExtension<MySchema>` to opt in.
@@ -190,7 +198,7 @@ export interface ExtensionContext<DB = any> {
190
198
  * Use for legitimately global operations (platform-wide reporting, backup).
191
199
  * Declaring `db:admin` surfaces the cross-tenant access at review + install.
192
200
  */
193
- adminDb?: Kysely<DB>;
201
+ adminDb?: 'db:admin' extends C ? Kysely<DB> : never;
194
202
  /**
195
203
  * Per-request, tenant-scoped database. Returns the request's tenant
196
204
  * transaction (so the `zveltio.current_tenant` GUC is set and FORCE-RLS rows
@@ -307,7 +315,7 @@ export interface ExtensionContext<DB = any> {
307
315
  * Engine-internal helpers. Stable across patch versions but may break across
308
316
  * minor versions. First-party extensions only.
309
317
  */
310
- internals: ExtensionInternals<DB>;
318
+ internals: Omit<ExtensionInternals<DB>, MembersLockedFor<C>>;
311
319
  }
312
320
  /**
313
321
  * Engine-internal helpers passed via `ctx.internals.*`.
@@ -383,6 +391,12 @@ export interface ExtensionInternals<DB = unknown> {
383
391
  * like a request's: the tenant GUC is set and `SET LOCAL ROLE` applied, so
384
392
  * the isolation policies bind.
385
393
  *
394
+ * Only the tenant the work already runs as — the request's, or the job's —
395
+ * is entered without a capability. Another tenant, or any tenant where none
396
+ * runs (load time, a timer), needs `tenant:enter` (or `db:admin`, which
397
+ * implies it); without it the promise rejects.
398
+ * A fire-and-forget job started from a handler keeps the handler's tenant.
399
+ *
386
400
  * await ctx.internals.withTenantIsolation(tenantId, async (db) => {
387
401
  * await db.selectFrom('zvd_things').selectAll().execute();
388
402
  * });
@@ -475,7 +489,12 @@ export interface ExtensionInternals<DB = unknown> {
475
489
  hidden: Set<string>;
476
490
  readOnly: Set<string>;
477
491
  }>;
478
- /** The Casbin role behind a user — what `getColumnAccess` keys on. */
492
+ /**
493
+ * The single role behind a user — what `getColumnAccess` keys on. Drawn from
494
+ * Casbin, the one source of roles: `god` (the instance attribute) > `member`
495
+ * (the `g <user> member *` row every account gets) > `public`. A user's full
496
+ * role set is `getUserRoles`.
497
+ */
479
498
  resolveUserRole: (user: {
480
499
  id?: string;
481
500
  role?: string;
@@ -542,6 +561,71 @@ export interface ExtensionInternals<DB = unknown> {
542
561
  */
543
562
  getUserNames: (userIds: string[]) => Promise<Record<string, string>>;
544
563
  isTenantAdmin: (userId: string) => Promise<boolean>;
564
+ /**
565
+ * Facts about the tenant this work runs as, scoped by the engine — the path
566
+ * in place of raw SQL on `"user"`, `zv_tenant_users`, `zv_tenants`,
567
+ * `pg_class`, `zvd_permissions` and `zv_settings`, which `ctx.db` refuses.
568
+ */
569
+ /** People with a membership in force in the running tenant, and its owners/admins. */
570
+ countMembers: () => Promise<{
571
+ total: number;
572
+ admins: number;
573
+ }>;
574
+ /** Collection count, and a planner row estimate (`null` on a multi-tenant instance). */
575
+ getDataStats: () => Promise<{
576
+ collections: number;
577
+ records_estimate: number | null;
578
+ }>;
579
+ /** Role names granted in the running tenant or in every tenant. */
580
+ listRoles: () => Promise<string[]>;
581
+ /** A setting the instance marks public (`is_public`); `null` otherwise. */
582
+ getPublicSetting: (key: string) => Promise<unknown>;
583
+ /** Append to the audit log, as a row of the running tenant (instance-level where none runs). The engine stamps `metadata.extension` with the caller. */
584
+ audit: (event: {
585
+ type: string;
586
+ userId?: string;
587
+ resourceId?: string;
588
+ resourceType?: string;
589
+ metadata?: Record<string, unknown>;
590
+ ip?: string;
591
+ }) => Promise<void>;
592
+ /**
593
+ * Run `fn` acting as the system inside the running tenant, for the named
594
+ * collections (`['products']`, not `zvd_products`): collection permissions
595
+ * stand down for `ctx.db` during the call; tenant isolation and the table
596
+ * guard do not. Needs the `data:system` capability. Every call is audited
597
+ * with the extension, the user, the collections and `reason`.
598
+ */
599
+ asSystem: <T>(collections: readonly string[], fn: () => Promise<T>, opts?: {
600
+ reason?: string;
601
+ }) => Promise<T>;
602
+ /**
603
+ * The running tenant's recent audit rows, newest first — never another
604
+ * tenant's, never instance-level events. `limit` defaults to 20, capped at
605
+ * 100. No `metadata` or `ip`. Needs the `audit:read` capability.
606
+ */
607
+ readAuditActivity: (query?: {
608
+ limit?: number;
609
+ eventType?: string;
610
+ resourceType?: string;
611
+ since?: Date | string;
612
+ }) => Promise<{
613
+ id: string;
614
+ event_type: string;
615
+ user_id: string | null;
616
+ resource_type: string | null;
617
+ resource_id: string | null;
618
+ created_at: Date;
619
+ }[]>;
620
+ /**
621
+ * How many of the running tenant's audit rows match, from `since` on. Same
622
+ * scope as `readAuditActivity`; a number, so ungated.
623
+ */
624
+ countAuditActivity: (query: {
625
+ since: Date | string;
626
+ eventType?: string;
627
+ resourceType?: string;
628
+ }) => Promise<number>;
545
629
  /**
546
630
  * Instance-level admin, as distinct from admin-within-a-tenant.
547
631
  *
@@ -558,7 +642,10 @@ export interface ExtensionInternals<DB = unknown> {
558
642
  extensionRegistry: any;
559
643
  /** Queue an HTML→PDF render via the worker pool. */
560
644
  generatePDFAsync: (html: string, options?: Record<string, unknown>) => Promise<unknown>;
561
- /** Move a file to the trash bucket (soft delete with TTL). */
645
+ /**
646
+ * Move a file to the trash bucket (soft delete with TTL): `(db, fileId, deletedBy)`.
647
+ * Only a file of the tenant the request or job runs as; outside one it rejects.
648
+ */
562
649
  moveToTrash: (...args: any[]) => Promise<unknown>;
563
650
  /** Enqueue an asynchronous DDL job (Ghost Tables, large alters). */
564
651
  enqueueDDLJob: (...args: any[]) => Promise<unknown>;
@@ -681,6 +768,47 @@ export interface ExtensionInternals<DB = unknown> {
681
768
  * cannot reach either. Needs the `auth:users` capability.
682
769
  */
683
770
  revokeUserSessions: (userId: string) => Promise<void>;
771
+ /**
772
+ * What the engine's own tables hold about a user — profile, their audit
773
+ * rows in every tenant and at instance level, notifications, the API keys
774
+ * they created (names, never secrets) and their approval requests — for a
775
+ * data-subject access request. `null` when there is no such user. Needs the
776
+ * `auth:users` capability.
777
+ */
778
+ exportUserData: (userId: string) => Promise<{
779
+ profile: {
780
+ id: string;
781
+ name: string | null;
782
+ email: string;
783
+ created_at: string;
784
+ } | null;
785
+ audit_log: Array<{
786
+ action: string;
787
+ collection: string | null;
788
+ record_id: string | null;
789
+ created_at: string;
790
+ }>;
791
+ notifications: Array<{
792
+ title: string;
793
+ message: string | null;
794
+ type: string | null;
795
+ is_read: boolean;
796
+ created_at: string;
797
+ }>;
798
+ api_keys: Array<{
799
+ name: string;
800
+ key_prefix: string | null;
801
+ scopes: unknown;
802
+ created_at: string;
803
+ }>;
804
+ approval_requests: Array<{
805
+ id: string;
806
+ collection: string;
807
+ record_id: string | null;
808
+ status: string;
809
+ requested_at: string;
810
+ }>;
811
+ } | null>;
684
812
  /**
685
813
  * `false` stops the user signing in by ANY method (password, magic link,
686
814
  * passkey, OAuth, SSO) and revokes their sessions; `true` lets them back in
@@ -702,6 +830,100 @@ export interface ExtensionInternals<DB = unknown> {
702
830
  * `auth:users` capability.
703
831
  */
704
832
  liftOwnBan: (db: unknown, userId: string) => Promise<boolean>;
833
+ /** Whether at most one tenant exists — then every user belongs to it. Ungated. */
834
+ isSingleTenantInstance: () => Promise<boolean>;
835
+ /**
836
+ * The account for `email` (case-insensitive), created when there is none:
837
+ * verified, no password, never god, in no tenant. Works while
838
+ * self-registration is off. Inside `withTenantIsolation` it is created in
839
+ * that transaction, and a rollback takes it back. An account that already
840
+ * exists comes back only if the running tenant has a claim to it: a member
841
+ * here with no power elsewhere beyond plain membership, any account but god
842
+ * and the instance admins on a single-tenant instance, or a bare record (no
843
+ * credential, membership or grant). Anyone else is `account_exists`: they join
844
+ * through an invitation, which proves they control the address.
845
+ */
846
+ provisionUser: (input: {
847
+ email: string;
848
+ name?: string;
849
+ }) => Promise<{
850
+ user: ProvisionedUser;
851
+ created: boolean;
852
+ }>;
853
+ /**
854
+ * Users of the running tenant — a membership there, lapsed included — or
855
+ * every user on a single-tenant instance (`membership: null`). By creation
856
+ * order; `limit` defaults to 100, at most 1000.
857
+ */
858
+ listTenantUsers: (db: unknown, query?: {
859
+ email?: string;
860
+ userId?: string;
861
+ limit?: number;
862
+ offset?: number;
863
+ }) => Promise<Array<ProvisionedUser & {
864
+ membership: TenantMembershipInfo | null;
865
+ }>>;
866
+ /**
867
+ * Rename or re-address a user the running tenant ALONE holds: a membership
868
+ * here (any user on a single-tenant instance), not god, not an instance admin,
869
+ * no membership or grant in any other tenant (`user_not_owned` otherwise).
870
+ * `db` is your transaction.
871
+ */
872
+ updateUserProfile: (db: unknown, userId: string, patch: {
873
+ name?: string;
874
+ email?: string;
875
+ }) => Promise<ProvisionedUser>;
876
+ /**
877
+ * Make the user a `member` (default) or `viewer` of the running tenant, or
878
+ * switch between the two, with the matching tenant role. Dates are kept: a
879
+ * membership that ended is not reopened. An `owner`/`admin` is refused.
880
+ */
881
+ addTenantMember: (db: unknown, userId: string, role?: 'member' | 'viewer') => Promise<'added' | 'role_changed' | 'unchanged'>;
882
+ /**
883
+ * Remove the user from the running tenant with every role they hold in it.
884
+ * `orphaned`: no tenant and nothing else holds the account now — deleting it
885
+ * (`deleteUser`, `auth:users`) is your call. `inForceAnywhere`: a membership
886
+ * in force remains in some tenant. An owner or admin of the tenant (in the
887
+ * default tenant, an instance admin) is refused, `role_not_allowed`.
888
+ */
889
+ removeTenantMember: (db: unknown, userId: string) => Promise<{
890
+ removed: boolean;
891
+ orphaned: boolean;
892
+ inForceAnywhere: boolean;
893
+ }>;
894
+ /**
895
+ * Set when the user's membership of the running tenant ends: `'now'`, an
896
+ * ISO-8601 instant, or `null` (open-ended). `ifInForce` changes only a
897
+ * membership in force; `ifValidTo` only one whose end is still exactly that
898
+ * value. `null` when the user is no member here. An owner or admin is
899
+ * refused, `role_not_allowed`.
900
+ */
901
+ setTenantMembershipEnd: (db: unknown, userId: string, validTo: string | null, guard?: {
902
+ ifInForce?: boolean;
903
+ ifValidTo?: string | null;
904
+ }) => Promise<{
905
+ changed: boolean;
906
+ previousValidTo: string | null;
907
+ validTo: string | null;
908
+ inForceAnywhere: boolean;
909
+ } | null>;
910
+ }
911
+ /** A user as `ctx.internals` identity provisioning returns it. */
912
+ export interface ProvisionedUser {
913
+ id: string;
914
+ email: string;
915
+ name: string;
916
+ emailVerified: boolean;
917
+ createdAt: string;
918
+ updatedAt: string;
919
+ }
920
+ /** A user's membership of the running tenant. */
921
+ export interface TenantMembershipInfo {
922
+ role: 'owner' | 'admin' | 'member' | 'viewer';
923
+ validFrom: string;
924
+ /** `null`: open-ended. */
925
+ validTo: string | null;
926
+ inForce: boolean;
705
927
  }
706
928
  /**
707
929
  * Entity-access scope handed to each extension via `ctx.entityAccess`.
@@ -776,18 +998,32 @@ export interface QueryAlterScope {
776
998
  * `register()` is idempotent within a scope (replaces on duplicate), so an
777
999
  * extension's `register()` can safely run multiple times during hot-reload.
778
1000
  */
1001
+ /**
1002
+ * A manifest `dependencies` / `optionalDependencies` entry. A required one must be
1003
+ * installed for this extension to load; an optional one need not be — its
1004
+ * services then read as `null`. Either way, the extensions named in these two
1005
+ * lists are the only ones whose services `ctx.services` hands this one.
1006
+ */
1007
+ export interface ManifestDependency {
1008
+ name: string;
1009
+ minVersion?: string;
1010
+ }
779
1011
  export interface ServiceRegistry {
780
1012
  /**
781
- * Publish a service under a name.
1013
+ * Publish a service under a name, which must be `<this extension's name>.<x>`.
782
1014
  * Idempotent: re-registering the same name from the same extension replaces.
783
1015
  * Throws if a different extension already owns that name.
784
1016
  */
785
1017
  register<T = unknown>(name: string, value: T): void;
786
1018
  /** Remove a service this extension previously registered. No-op if not owned. */
787
1019
  unregister(name: string): void;
788
- /** Get a service. Returns `null` if not registered. */
1020
+ /**
1021
+ * Get a service. Returns `null` if not registered (an optional dependency
1022
+ * that is not installed). Throws if its owner is not this extension or one in
1023
+ * its manifest `dependencies` / `optionalDependencies`.
1024
+ */
789
1025
  get<T = unknown>(name: string): T | null;
790
- /** Check if a service is registered. */
1026
+ /** Check if a service is registered. Throws as `get` does for an undeclared owner. */
791
1027
  has(name: string): boolean;
792
1028
  /**
793
1029
  * Wait for a service to be registered. Resolves immediately if already there.
@@ -912,7 +1148,7 @@ export type MountStrategy = 'global' | 'subapp';
912
1148
  * }
913
1149
  * };
914
1150
  */
915
- export interface ZveltioExtension<DB = any> {
1151
+ export interface ZveltioExtension<DB = any, C extends string = Capability> {
916
1152
  /** Unique name — must match manifest.json `name` exactly (e.g. `'hr/employees'`). */
917
1153
  name: string;
918
1154
  /** Category shown in the marketplace (e.g. `'hr'`, `'finance'`, `'content'`). */
@@ -927,7 +1163,7 @@ export interface ZveltioExtension<DB = any> {
927
1163
  * Called once when the extension is activated.
928
1164
  * Register Hono routes, subscribe to events, etc.
929
1165
  */
930
- register: (app: Hono, ctx: ExtensionContext<DB>) => Promise<void>;
1166
+ register: (app: Hono, ctx: ExtensionContext<DB, C>) => Promise<void>;
931
1167
  /** Register custom Studio field types contributed by this extension. */
932
1168
  registerFieldTypes?: (registry: FieldTypeRegistryAPI) => void;
933
1169
  /** Return absolute paths to SQL migration files, run in order on first activation. */
@@ -1057,5 +1293,21 @@ export interface AssetPreviewHandler {
1057
1293
  }) => boolean;
1058
1294
  component: any;
1059
1295
  }
1296
+ /**
1297
+ * Declare an extension together with the capabilities it uses, so `ctx` is typed
1298
+ * by them (roadmap R8). The list must match `permissions` in manifest.json —
1299
+ * the manifest is what the engine enforces and an administrator approves;
1300
+ * `zveltio extension validate` reports a difference.
1301
+ *
1302
+ * export default defineExtension(['secrets'], { name, category, register });
1303
+ *
1304
+ * Returns the extension unchanged, with the list attached as `capabilities`.
1305
+ */
1306
+ /** `ZveltioExtension`'s own default schema type, without spelling it again. */
1307
+ type DefaultExtensionDB = ZveltioExtension extends ZveltioExtension<infer D> ? D : never;
1308
+ export declare function defineExtension<const C extends readonly Capability[], DB = DefaultExtensionDB>(capabilities: C, extension: ZveltioExtension<DB, C[number]>): ZveltioExtension<DB, C[number]> & {
1309
+ readonly capabilities: C;
1310
+ };
1060
1311
  export type { Hono };
1312
+ export * from './capabilities.js';
1061
1313
  //# sourceMappingURL=index.d.ts.map