@happyvertical/smrt-users 0.40.70 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -0
- package/dist/chunks/{TerminalAuthService-C1PVcd6q.js → TerminalAuthService-BfX2Ujqe.js} +664 -643
- package/dist/chunks/TerminalAuthService-BfX2Ujqe.js.map +1 -0
- package/dist/collections/CliAuthRequestCollection.d.ts +10 -1
- package/dist/collections/CliAuthRequestCollection.d.ts.map +1 -1
- package/dist/collections/MagicLinkTokenCollection.d.ts +10 -1
- package/dist/collections/MagicLinkTokenCollection.d.ts.map +1 -1
- package/dist/collections/SessionCollection.d.ts +10 -2
- package/dist/collections/SessionCollection.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -101
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +390 -205
- package/dist/models/CliAuthRequest.d.ts +18 -3
- package/dist/models/CliAuthRequest.d.ts.map +1 -1
- package/dist/models/MagicLinkToken.d.ts +6 -1
- package/dist/models/MagicLinkToken.d.ts.map +1 -1
- package/dist/models/Session.d.ts +3 -0
- package/dist/models/Session.d.ts.map +1 -1
- package/dist/models/User.d.ts +7 -1
- package/dist/models/User.d.ts.map +1 -1
- package/dist/retention.d.ts +47 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +22 -5
- package/dist/sveltekit.js +1 -1
- package/package.json +9 -9
- package/dist/chunks/TerminalAuthService-C1PVcd6q.js.map +0 -1
|
@@ -4,9 +4,19 @@ import { SmrtObject } from '@happyvertical/smrt-core';
|
|
|
4
4
|
*/
|
|
5
5
|
export type CliAuthRequestStatus = 'pending' | 'approved' | 'expired';
|
|
6
6
|
export declare class UsersCliAuthRequest extends SmrtObject {
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Short, human-typeable code the user enters in the browser to approve the session.
|
|
9
|
+
*
|
|
10
|
+
* Indexed: `findByUserCode()` looks requests up by this column (#2364,
|
|
11
|
+
* epic #2382 finding A3).
|
|
12
|
+
*/
|
|
8
13
|
userCode: string;
|
|
9
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* SHA-256 hash of the long device code the CLI keeps secret.
|
|
16
|
+
*
|
|
17
|
+
* Indexed: `findByDeviceCodeHash()` — the CLI's poll loop — looks requests
|
|
18
|
+
* up by this column (#2364, epic #2382 finding A3).
|
|
19
|
+
*/
|
|
10
20
|
deviceCodeHash: string;
|
|
11
21
|
/** Lifecycle state — `pending` → `approved` | `expired`. */
|
|
12
22
|
status: CliAuthRequestStatus;
|
|
@@ -16,7 +26,12 @@ export declare class UsersCliAuthRequest extends SmrtObject {
|
|
|
16
26
|
tenantId: string;
|
|
17
27
|
/** Session id minted on approval — handed to the CLI as its bearer token. */
|
|
18
28
|
sessionId: string;
|
|
19
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* When the pending request stops accepting approvals.
|
|
31
|
+
*
|
|
32
|
+
* Indexed: `deleteExpired()` — and the retention sweep that now schedules
|
|
33
|
+
* it (#2375) — scans this column on every pass.
|
|
34
|
+
*/
|
|
20
35
|
expiresAt: Date;
|
|
21
36
|
/** When approval happened — null while the request is still pending. */
|
|
22
37
|
approvedAt: Date | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CliAuthRequest.d.ts","sourceRoot":"","sources":["../../src/models/CliAuthRequest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAS,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtE,qBAMa,mBAAoB,SAAQ,UAAU;IACjD
|
|
1
|
+
{"version":3,"file":"CliAuthRequest.d.ts","sourceRoot":"","sources":["../../src/models/CliAuthRequest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAS,UAAU,EAAQ,MAAM,0BAA0B,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtE,qBAMa,mBAAoB,SAAQ,UAAU;IACjD;;;;;OAKG;IAEH,QAAQ,SAAM;IAEd;;;;;OAKG;IAEH,cAAc,SAAM;IAEpB,4DAA4D;IAE5D,MAAM,EAAE,oBAAoB,CAAa;IAEzC,uEAAuE;IAEvE,MAAM,SAAM;IAEZ,uEAAuE;IAEvE,QAAQ,SAAM;IAEd,6EAA6E;IAE7E,SAAS,SAAM;IAEf;;;;;OAKG;IAEH,SAAS,OAAc;IAEvB,wEAAwE;IAExE,UAAU,EAAE,IAAI,GAAG,IAAI,CAAQ;CAChC;AAED,OAAO,EAAE,mBAAmB,IAAI,cAAc,EAAE,CAAC"}
|
|
@@ -20,7 +20,12 @@ export declare class UsersMagicLinkToken extends SmrtObject {
|
|
|
20
20
|
email: string;
|
|
21
21
|
/** Whether this token has been used */
|
|
22
22
|
used: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* When this token expires.
|
|
25
|
+
*
|
|
26
|
+
* Indexed: `deleteExpired()` — and the retention sweep that now schedules
|
|
27
|
+
* it (#2375) — scans this column on every pass.
|
|
28
|
+
*/
|
|
24
29
|
expiresAt: Date;
|
|
25
30
|
constructor(options?: MagicLinkTokenOptions);
|
|
26
31
|
/** Check if the token has expired */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MagicLinkToken.d.ts","sourceRoot":"","sources":["../../src/models/MagicLinkToken.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC;;GAEG;AACH,eAAO,MAAM,4BAA4B,QAAU,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;CACpC;AAED,qBAOa,mBAAoB,SAAQ,UAAU;IACjD,8CAA8C;IAE9C,KAAK,EAAE,MAAM,CAAM;IAEnB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAM;IAEnB,uCAAuC;IACvC,IAAI,EAAE,OAAO,CAAS;IAEtB
|
|
1
|
+
{"version":3,"file":"MagicLinkToken.d.ts","sourceRoot":"","sources":["../../src/models/MagicLinkToken.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC;;GAEG;AACH,eAAO,MAAM,4BAA4B,QAAU,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;CACpC;AAED,qBAOa,mBAAoB,SAAQ,UAAU;IACjD,8CAA8C;IAE9C,KAAK,EAAE,MAAM,CAAM;IAEnB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAM;IAEnB,uCAAuC;IACvC,IAAI,EAAE,OAAO,CAAS;IAEtB;;;;;OAKG;IAEH,SAAS,EAAE,IAAI,CAA8D;gBAEjE,OAAO,GAAE,qBAA0B;IAa/C,qCAAqC;IACrC,SAAS,IAAI,OAAO;IAIpB,iEAAiE;IACjE,OAAO,IAAI,OAAO;CAGnB;AAED,OAAO,EAAE,mBAAmB,IAAI,cAAc,EAAE,CAAC"}
|
package/dist/models/Session.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export declare class Session extends SmrtObject {
|
|
|
57
57
|
status: SessionStatus;
|
|
58
58
|
/**
|
|
59
59
|
* Session expiration time
|
|
60
|
+
*
|
|
61
|
+
* Indexed: `deleteExpired()` — and the retention sweep that now schedules it
|
|
62
|
+
* (#2375) — scans this column on every pass.
|
|
60
63
|
*/
|
|
61
64
|
expiresAt: Date;
|
|
62
65
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Session.d.ts","sourceRoot":"","sources":["../../src/models/Session.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,mEAAmE;IACnE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAAmB,CAAC;AAEpD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAMa,OAAQ,SAAQ,UAAU;IACrC;;OAEG;IAEH,MAAM,EAAE,MAAM,CAAM;IAEpB;;;OAGG;IAEH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B;;OAEG;IAEH,MAAM,EAAE,aAAa,CAAwB;IAE7C
|
|
1
|
+
{"version":3,"file":"Session.d.ts","sourceRoot":"","sources":["../../src/models/Session.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,mEAAmE;IACnE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,cAAc,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAAmB,CAAC;AAEpD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAMa,OAAQ,SAAQ,UAAU;IACrC;;OAEG;IAEH,MAAM,EAAE,MAAM,CAAM;IAEpB;;;OAGG;IAEH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B;;OAEG;IAEH,MAAM,EAAE,aAAa,CAAwB;IAE7C;;;;;OAKG;IAEH,SAAS,EAAE,IAAI,CAAc;IAE7B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAM;IAEvB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAM;IAEvB;;OAEG;IACH,cAAc,EAAE,IAAI,CAAc;IAElC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAM;gBAEvB,OAAO,GAAE,cAAmB;IAsBxC;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,UAAU,GAAE,MAA4B,GAAG,IAAI;IAKtD;;OAEG;IACH,MAAM,IAAI,IAAI;IAId;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIxC;;OAEG;IACH,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAItC;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAI1C;;OAEG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAG9B"}
|
package/dist/models/User.d.ts
CHANGED
|
@@ -44,7 +44,13 @@ export declare class User extends SmrtObject implements UserContract {
|
|
|
44
44
|
*/
|
|
45
45
|
profileId: string;
|
|
46
46
|
/**
|
|
47
|
-
* User's email address (unique, used for lookup)
|
|
47
|
+
* User's email address (unique, used for lookup).
|
|
48
|
+
*
|
|
49
|
+
* Indexed: `findByEmail()` filters on this raw column directly (#2364,
|
|
50
|
+
* epic #2382 finding A3). Distinct from `emailKey`'s unique index below —
|
|
51
|
+
* `emailKey` is a separately normalized column (`normalizeIdentityEmail()`)
|
|
52
|
+
* that arbitrates cross-connection uniqueness, and its index does not serve
|
|
53
|
+
* a read path that filters on `email` itself.
|
|
48
54
|
*/
|
|
49
55
|
email: string;
|
|
50
56
|
/** Durable normalized key for cross-connection email uniqueness. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"User.d.ts","sourceRoot":"","sources":["../../src/models/User.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC3B;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAOa,IAAK,SAAQ,UAAW,YAAW,YAAY;IAC1D;;OAEG;IAEH,SAAS,EAAE,MAAM,CAAM;IAEvB
|
|
1
|
+
{"version":3,"file":"User.d.ts","sourceRoot":"","sources":["../../src/models/User.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC3B;AAQD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAOa,IAAK,SAAQ,UAAW,YAAW,YAAY;IAC1D;;OAEG;IAEH,SAAS,EAAE,MAAM,CAAM;IAEvB;;;;;;;;OAQG;IAEH,KAAK,EAAE,MAAM,CAAM;IAEnB,oEAAoE;IAEpE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B;;OAEG;IAEH,MAAM,EAAE,UAAU,CAAqB;IAEvC;;OAEG;IACH,WAAW,EAAE,IAAI,GAAG,IAAI,CAAQ;gBAEpB,OAAO,GAAE,WAAgB;IASrC;;;OAGG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB,sEAAsE;IACvD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential retention — expired sessions and tokens (issue #2375, finding F2).
|
|
3
|
+
*
|
|
4
|
+
* `SessionCollection.deleteExpired()`, `MagicLinkTokenCollection.deleteExpired()`
|
|
5
|
+
* and `CliAuthRequestCollection.deleteExpired()` all existed, and every one of
|
|
6
|
+
* them waited for an application to remember to call it. Expired credential
|
|
7
|
+
* rows are the worst kind of unbounded growth: they are worthless the moment
|
|
8
|
+
* they expire and they are exactly the rows an attacker would like to still
|
|
9
|
+
* find in the table.
|
|
10
|
+
*
|
|
11
|
+
* This module contributes them to the framework retention sweep, so
|
|
12
|
+
* `smrt db:prune` and the jobs `TaskRunner`'s periodic sweep reap them like
|
|
13
|
+
* every other framework-owned table.
|
|
14
|
+
*
|
|
15
|
+
* The package entry point registers all three on import (see `index.ts`), so
|
|
16
|
+
* any process that loaded `@happyvertical/smrt-users` contributes them.
|
|
17
|
+
* Registration is not scheduling: nothing is deleted until something runs a
|
|
18
|
+
* sweep, and a policy can turn any task off by name. Task names are prefixed
|
|
19
|
+
* with the owning package's short name, because the registry is one
|
|
20
|
+
* process-global namespace shared with every other package.
|
|
21
|
+
*
|
|
22
|
+
* @see https://github.com/happyvertical/smrt/issues/2375
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
/** Retention task name for expired and revoked sessions. */
|
|
26
|
+
export declare const SESSIONS_RETENTION_TASK = "users-sessions";
|
|
27
|
+
/** Retention task name for expired magic-link tokens. */
|
|
28
|
+
export declare const MAGIC_LINK_RETENTION_TASK = "users-magic-link-tokens";
|
|
29
|
+
/** Retention task name for expired CLI device-code requests. */
|
|
30
|
+
export declare const CLI_AUTH_RETENTION_TASK = "users-cli-auth-requests";
|
|
31
|
+
/** Every task name {@link registerUserRetentionTasks} installs. */
|
|
32
|
+
export declare const USER_RETENTION_TASKS: readonly ["users-sessions", "users-magic-link-tokens", "users-cli-auth-requests"];
|
|
33
|
+
/**
|
|
34
|
+
* Register session and token expiry with the framework retention sweep.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent: re-registering replaces the previous tasks.
|
|
37
|
+
*
|
|
38
|
+
* Each task deletes only rows that have already expired — there is no
|
|
39
|
+
* retention window to configure, because an expired credential has no value
|
|
40
|
+
* to retain. Applications that keep expired sessions for audit should opt the
|
|
41
|
+
* task out (`tasks: { 'users-sessions': false }`) and archive them
|
|
42
|
+
* themselves.
|
|
43
|
+
*/
|
|
44
|
+
export declare function registerUserRetentionTasks(): void;
|
|
45
|
+
/** Remove the credential retention tasks from the framework sweep. */
|
|
46
|
+
export declare function unregisterUserRetentionTasks(): void;
|
|
47
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../src/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAUH,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,mBAAmB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AAEnE,gEAAgE;AAChE,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,mFAIvB,CAAC;AAEX;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CA2BjD;AAED,sEAAsE;AACtE,wBAAgB,4BAA4B,IAAI,IAAI,CAInD"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-users",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.42.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "59c5b2483bf29b0b142307c155b865574195fda90f2be26ce40856cbddff9090",
|
|
11
|
+
"packageJson": "ca4107324dc0c7cbf7328f339338f2b5300532baf2bd395cdfbb91500e571694",
|
|
12
|
+
"agents": "6ec265b8af7cdfa9fea1a3f128af9bf25cbcdb1353c34a81a7fc5b0d2e4a8f2b",
|
|
13
|
+
"moduleDoc:agents/retention.md": "455e7e9d36e8410b5deb62aa274421c50c8f2037d04bf770145d2fc71b79030f"
|
|
13
14
|
},
|
|
14
15
|
"exports": [
|
|
15
16
|
".",
|
|
@@ -130,6 +131,9 @@
|
|
|
130
131
|
{
|
|
131
132
|
"name": "deleteExpired",
|
|
132
133
|
"async": true,
|
|
134
|
+
"params": [
|
|
135
|
+
"options?: object"
|
|
136
|
+
],
|
|
133
137
|
"returns": "Promise<number>"
|
|
134
138
|
},
|
|
135
139
|
{
|
|
@@ -386,6 +390,9 @@
|
|
|
386
390
|
{
|
|
387
391
|
"name": "deleteExpired",
|
|
388
392
|
"async": true,
|
|
393
|
+
"params": [
|
|
394
|
+
"options?: object"
|
|
395
|
+
],
|
|
389
396
|
"returns": "Promise<number>"
|
|
390
397
|
},
|
|
391
398
|
{
|
|
@@ -876,6 +883,9 @@
|
|
|
876
883
|
{
|
|
877
884
|
"name": "deleteExpired",
|
|
878
885
|
"async": true,
|
|
886
|
+
"params": [
|
|
887
|
+
"options?: object"
|
|
888
|
+
],
|
|
879
889
|
"returns": "Promise<number>"
|
|
880
890
|
},
|
|
881
891
|
{
|
|
@@ -4653,5 +4663,12 @@
|
|
|
4653
4663
|
"polymorphicAssociations": 0,
|
|
4654
4664
|
"uuidColumns": 52
|
|
4655
4665
|
},
|
|
4656
|
-
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Models (14)\n\n| Model | Key Pattern |\n|-------|-------------|\n| User | Auth identity. `profileId` is a unique cross-package reference to smrt-profiles (one User per non-null Profile). Email auto-lowercased; readonly nullable unique `emailKey` is derived on save for durable normalized uniqueness. |\n| AccessRequest | \"Request access / waitlist\" record captured before a `User` exists. CLOSED generated surface (`api`/`mcp`/`cli` = `[]`) — all access via `AccessRequestService`. Email normalized + indexed; JSON `requestContext` (NOT `context` — reserved for slug scoping). |\n| Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |\n| Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |\n| MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |\n| Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. `inheritsToDescendants: true` (default false) opts the role's membership authority into descendant tenants. |\n| Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |\n| Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |\n| Group | Team within a tenant. Multiple roles via GroupRole. |\n| GroupMember, GroupRole, RolePermission | Join tables. |\n| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |\n| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |\n\n## Permission Resolution — Precedence (broad → specific, most-specific wins)\n\n`PermissionResolver.resolvePermissions` builds the effective set in this order;\neach later layer overrides earlier ones:\n\n1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`\n down the cascade (GRANT adds, DENY removes within the hierarchy)\n2. **Membership role** — base permissions from the user's role in the tenant\n3. **Group roles** — permissions from all groups the user belongs to **in that tenant**\n4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a\n `TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it\n subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It\n sits just **above** the per-user membership overrides and **below** role/group.\n5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can\n re-add a slug a tenant DENY'd in step 4, because it is more specific.\n6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes\n the slug last and is never overridden.\n\nSo a permission a role grants but the tenant DENYs is **removed**, unless that\nexact user also has a membership-GRANT override for it. A membership-DENY always\nwins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).\nThe hard block reflects the tenant cascade's **net** resolution, not an\nunconditional union of every DENY in the chain — so a more-specific tenant GRANT\n(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.\n\n### Membership selection — hierarchical inheritance (opt-in, #1866)\n\nWhich membership feeds step 2 above:\n\n1. **A direct membership row in the target tenant always pins resolution to\n itself** — active rows resolve normally; an inactive (pending/suspended)\n row resolves to the **empty set**. A direct row therefore *attenuates*\n rather than unions: \"viewer here, despite being network admin\" gives\n viewer, and a suspension in the child is effective even for users holding\n inheritable authority on an ancestor.\n2. **No direct row** → the resolver walks the tenant's ancestors (nearest\n first, from `hierarchyPath`) and resolves through the **nearest ACTIVE\n ancestor membership whose role has `inheritsToDescendants: true`**.\n Unflagged or inactive ancestor memberships are skipped (they neither confer\n nor block); there is **no union across the chain** — the nearest flagged\n membership alone is used. To attenuate a specific descendant, create a\n direct membership there or use a tenant-level DENY.\n3. **No qualifying ancestor** → empty set (byte-identical to the\n pre-inheritance resolver; with no role flagged, nothing changes).\n\nAll later layers run unchanged against the **target** tenant: the tenant\ncascade and tenant-DENY hard block come from the target tenant (a child can\ncarve authority out of an inherited role), group roles stay exact-tenant (only\ntarget-tenant groups contribute; ancestor groups never flow down), and\nmembership GRANT/DENY overrides travel with the ancestor membership used.\n`PermissionResolutionResult.inheritedFromTenantId` reports the ancestor tenant\nwhen inheritance was used (`null` for direct resolution).\n\nSafety: resolution is bounded by `MAX_TENANT_HIERARCHY_DEPTH`; malformed\n`hierarchyPath` values fail closed to the empty set — too deep,\nself-referential, duplicate ancestors, or inconsistent with the actual\n`parentTenantId` chain (the path is verified link-by-link against the loaded\nancestor rows before it is trusted as an authorization source). Tenant `status` is not consulted (parity with direct\nresolution). Caching: a long-lived `(user, tenant)` permission cache must also\ninvalidate on ancestor-membership changes and on `Role.inheritsToDescendants`\nflips — request-scoped caches (the common pattern) are unaffected.\n\nFlag roles at seed time with\n`seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] })` (additive:\nlisted slugs are flagged, omitted slugs are never unflagged; unknown slugs\nthrow). The default seed leaves every role exact-tenant.\n\n## Operation Permission Guards\n\nSMRT derives a fine-grained, per-`<collection>.<action>` permission catalog **from the\nmanifest** (`PermissionCatalogService`, including custom `@smrt` actions — not just CRUD)\nand enforces it two ways: app-side via `assertOperationPermission()` / `PermissionResolver`,\nand at the database via generated **Postgres RLS** policies\n(`generatePostgresPermissionSql()` / `applyPostgresPermissionPolicies()`) that read the\nsession-injected `smrt.permissions` / `smrt.tenant_id` (set by `withSessionPermissionContext`\nvia `set_config`). Because the RLS teeth are at the data layer, enforcement is door-agnostic\n— REST, MCP, and in-process callers are all bounded once the principal's context is set.\nSetup: README → *Manifest-derived permission catalog* and *Postgres RLS enforcement*.\n\n- Use `assertOperationPermission()` for hand-written mutations in SvelteKit form\n actions, custom endpoints, CLI scripts, and jobs. It derives the same\n `<collection>.<action>` slugs as `PermissionCatalogService` (`list`/`get` →\n `read`), requires the slug to exist in the catalog, then resolves permissions\n through `PermissionResolver`.\n- `assertOperationPermission()` throws fail-closed by default. Use\n `{ onDeny: 'return' }`, `checkOperationPermission()`, or\n `hasOperationPermission()` when a structured/boolean result is needed.\n- **Resource-tenant calling convention**: for resource-anchored authorization,\n pass the **resource's** tenant id — `tenantId: resourceTenantId` — not the\n session's current tenant. With `Role.inheritsToDescendants` flagged, a\n root-tenant admin then passes for any descendant resource with no app-side\n authority logic (and no membership fan-out), while per-child delegation and\n DENY precedence keep working. Do NOT pass a session-scoped `membership`\n alongside a different resource `tenantId` — a membership/tenant mismatch\n fails closed by design; omit `membership` and let the resolver look it up.\n- System context and super-admin bypass context are honored for parity with\n Postgres RLS. Pass `{ allowSuperAdminBypass: false }` on money-class or\n separation-of-duties operations that must require an explicit permission grant.\n- **Postgres RLS and membership inheritance**: RLS policies check the\n session-injected `smrt.permissions` list (resolved app-side by\n `PermissionResolver`), so a session whose `smrt.tenant_id` IS the child\n tenant gets inherited authority in RLS automatically. But RLS row filtering\n stays bound to the session's tenant setting — a root-tenant session acting\n on child-tenant rows is authorized by the app-level guard (resource-tenant\n convention above), not by RLS. This is a documented divergence, mirroring\n how #1829 handled bypass parity.\n- Seed role mappings with `RolePermissionCollection.seedRolePermissions()` or\n `RoleCollection.seedSystemRoles({ seedPermissions: true })`. The default\n matrix maps owner/admin to all catalog permissions, member to read/create for\n ordinary app resources, and viewer to read-only. Member create grants\n intentionally exclude users/RBAC authority and security resources (`users`,\n `tenants`, `roles`, `permissions`, memberships, groups, sessions, magic-link\n tokens, and related join/override tables). Re-seeding is additive and\n idempotent; pruning stale mappings requires `{ prune: true }`. When a\n contributing package registers a new built-in self-personalization\n permission after system roles already exist, call the explicit,\n idempotent `RolePermissionCollection.seedDefaultRolePersonalizationPermissions()`\n upgrade helper; it targets owner/admin/member/viewer only and never grants\n custom roles.\n\n**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.\n\n## Hierarchical Tenants\n\n- `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit\n- `moveToParent()` updates tenant + ALL descendants' paths/levels\n- `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true\n- `getTree(rootId?)` returns nested structure for UI\n- Two independent downward flows: the `TenantPermissionOverride` **cascade**\n (tenant-level permission config, flags above) and **membership-role\n inheritance** (`Role.inheritsToDescendants`, per-role opt-in — see\n \"Membership selection\" above). The cascade flags do not gate membership\n inheritance.\n\n## SvelteKit Integration\n\n```typescript\n// hooks.server.ts\nexport const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });\n// Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }\n\n// +page.server.ts\nawait createSessionCookie(event, userId, tenantId, { db });\nawait destroySessionCookie(event, { db });\nawait switchSessionTenant(event, tenantId, { db });\n```\n\n## Mobile `/api/mobile` Handlers (ADR 0001 Phase 3.5, #1748)\n\n`createMobileAuthHandlers(options)` (from `/sveltekit`) returns the mountable\nserver side of the KMP mobile contract: `authStart` (`POST auth/start`,\nserver-brokered PKCE via `OidcLoginService`), `authComplete`\n(`POST auth/complete`, code + echoed `state`/`codeVerifier` → bearer\nsession), `session.GET`/`session.DELETE` (bootstrap/logout), and\n`guard`/`withSession` — the bearer middleware for app-owned mobile routes.\nCore logic lives in `MobileAuthService` (framework-agnostic; exported from\nthe package root).\n\n- **Bearer = session id** (same convention as `TerminalAuthService`); 401\n bodies are `{ error, code }` and drive the mobile client's re-auth flow.\n- **Stateless handshake**: the OAuth `state` is an HMAC-signed token\n (secret: `stateSecret` ?? provider `clientSecret`) carrying\n nonce/provider/createdAt — full ID-token nonce verification with no\n server-side pending state. The `codeVerifier` never enters a URL: it is\n client-held per the frozen contract.\n- **Wire DTOs** come from `@happyvertical/smrt-mobile-contract`\n (`MobileAuthStartRequest` etc.) — one owning package for the Kotlin,\n Swift, and TypeScript shapes (compile-checked descriptors + parity test).\n- **Tenant options** honor `Role.inheritsToDescendants` (#1867): direct\n ACTIVE memberships plus descendants of flagged memberships (nearest\n flagged ancestor labels the option; any direct row pins; inactive direct\n rows exclude). Session binding defaults to the first DIRECT tenant —\n override with `resolveTenantId`.\n- **Hooks**: `resolveUser` (invite-gating; default provisions via\n `getOrCreateFromOidc`), `resolveTenantId`, `buildExtras` (bootstrap\n `extras`; model JSON must use `toPublicJSON({ permissions })` — #1822).\n `buildExtras` is the only app-domain bootstrap extension point. Never emit\n app fields such as `dashboard` at the response top level: they are outside\n `MobileSessionBootstrap` and the Kotlin decoder ignores them.\n- **Guard** wraps `withSessionPermissionContext`, so\n `assertOperationPermission`, tenancy context, and Postgres RLS all see the\n bearer caller; `OperationPermissionError` maps to 403 with a\n machine-readable `reason`.\n- **Uploads**: `resolveMobileUploadDedupKey` + the documented contract in\n `docs/content/architecture/mobile-upload-contract.md` (`clientCaptureId`\n field, `Idempotency-Key` header fallback); domain ingestion stays\n app-side. Framework-model writes ride `sync/apply`, not this path.\n- Configure `redirectUris` in production — RFC 8252 scheme rules always\n apply, but the allow list is the defense against redirecting authorization\n responses to attacker-controlled URIs.\n\n## Security (S5 #1400)\n\n- **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**\n User, Tenant, Group, Membership, MembershipOverride, Role, Permission,\n RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate\n `list`/`get` only — `create`/`update`/`delete` are intentionally NOT\n generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,\n not *authorization*, and these models are not `@TenantScoped`, so an\n auto-generated mutating route would let any authenticated user self-grant a\n role/permission, flip a tenant's cascade flags, or change another user's auth\n identity. Mutate them through the permission-gated services (`TenantService`,\n collection helpers) or consumer-owned, permission-checked handlers. A\n structural regression test (`security-audit-1400.test.ts`) enumerates the\n registry to assert no authority model exposes a mutating op. (`cli` stays\n enabled — local-operator surface, outside the network/agent threat model.)\n- **`switchTenant` is fail-closed AND rotates the session id.**\n `SessionService.switchTenant` / `switchSessionTenant` verify the session's user\n has an ACTIVE membership in the target tenant before any write (the tenant id\n is the isolation key for every `@TenantScoped` query). A non-member/unknown-\n session switch returns `{ switched: false, sessionId: null, ... }` and mutates\n nothing. On a successful switch into a NON-null tenant the session id is\n ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,\n device context carried over) is minted and the old session is REVOKED — so a\n captured pre-switch id immediately stops validating, shrinking the blast radius\n of a leaked id across a tenant boundary. `switchTenant` returns a\n `SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST\n persist the returned `sessionId`. `switchSessionTenant` does this for you by\n re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new\n id. A `null` clear stays in place (no rotation, no cookie change). The\n low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used\n for the null-clear path) — never call it with an untrusted tenant id.\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n- **RFC 9207 response issuer is exact.** OIDC callbacks validate a supplied\n `iss` against discovered metadata with exact string comparison before trusting\n a code or error. If metadata advertises issuer-response support, `iss` is\n required.\n- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios\n live in\n `packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both\n package suites execute that matrix and public docs reference it rather than\n maintaining another behavioral table. Default provisioning reuses only one\n unowned, global `Person`. Tenant-scoped, non-Person, duplicate-email,\n and already-owned matches fail before User/session creation. An existing\n issuer/subject link without a User must still be the unique global Person for\n the current verified claim email; once owned, the stable issuer/subject link\n reuses its canonical Person and owner.\n Issuer and subject are opaque, case-sensitive identifiers; preserve their\n exact value and use trim only to reject blank claims.\n- **`resolveProfile` is the application reconciliation boundary.** The\n SvelteKit handlers, `OidcLoginService`, and `getOrCreateFromOidc` accept the\n same hook inside the provisioning transaction. The service/handler path\n supplies protocol-validated claims; direct collection callers must validate\n and trust their claim source before calling `getOrCreateFromOidc`.\n Token/userinfo merging keeps `email` and `email_verified` paired to the same\n claim source; verification is never borrowed across sources.\n Resolver reads/writes use the supplied `db`, and the hook must be idempotent\n because a concurrent unique-key conflict can retry it. `undefined` chooses\n the secure default and `null` rejects, including exact issuer/subject reuse.\n For a new identity, a supplied Profile is still validated as the unique,\n unowned global Person for the verified email. For an exact existing identity,\n it must be the already-linked Profile and cannot rebind identity authority;\n stable-link owner and canonical-Person checks still apply. The hook receives a\n separate frozen claims snapshot; internal retry and persistence state is not\n exposed for mutation.\n- **Owned first binding requires `authorizeProfileOwner`.** An invitation or\n approval workflow may explicitly return both its pre-provisioned canonical\n global `Person` and existing approved `User` from this transaction-bound\n hook. `undefined` keeps the secure `profile_owned` default and `null`\n rejects. SMRT treats only the selected IDs as input, reloads them in the\n provisioning transaction, and requires `email_verified === true`, the unique\n canonical global Person for the normalized claim email, exactly one owner,\n that owner as the selected User, and the same normalized User email. The\n hook runs before identity/User/session creation and may be retried, so use\n only its supplied `db` and `users` handles and keep application authorization\n idempotent. Never authorize from email matching alone. Existing exact\n identities cannot be rebound; when `resolveProfile` is also present both\n hooks must select the same Profile.\n- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one\n transaction. The database arbiters are `OidcIdentity.identityKey`,\n private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the\n unique `User.profileId`; local callbacks acquire exact issuer/subject and normalized\n email locks in deterministic order so changed email claims also serialize.\n SQLite and DuckDB callbacks additionally serialize every root-handle statement\n the coordinator owns per database URL — `_smrt_backfills` initialization, the\n transaction, and the post-commit rebind — because one adapter multiplexes a\n single native connection and cannot safely overlap unrelated root\n transactions. Never overlap two statements on one such handle, inside a\n transaction or not: rebind and owner/email candidate reads are sequential,\n never `Promise.all`. Owner-authorized DuckDB callbacks use that same\n root-handle serialization;\n PostgreSQL deadlock and serialization errors use a bounded transaction retry.\n Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP\n display names cannot trigger a natural-key upsert;\n run\n `smrt db:status`, `smrt db:migrate`, then `smrt db:status` before deployment.\n Stop or upgrade old writers first. Before migration, group\n non-null `users.profile_id` values, then reconcile duplicates. After\n migration, run public `backfillProfileEmailKeys(db)` followed by\n `backfillUserEmailKeys(db)` from one deploy process. Both use the shared\n TypeScript `normalizeIdentityEmail()` implementation transactionally and are\n idempotent; the User backfill fails before writes if normalized duplicates\n remain. Every OIDC path requires the Profile email-key readiness marker;\n creating a User or checking User email uniqueness additionally requires the\n User marker. A stable issuer/subject with an existing owning User skips only\n the User email-key lookup and marker. Full scans remain in the explicit deploy\n step; guarded runtime paths use indexed keys and validate only returned\n candidates. Multiple null links remain valid.\n Legacy race keys backfill only after canonical validation. Pass a root\n database on adapters such as DuckDB that cannot create nested savepoints.\n Root adapters must expose `beginTransaction`; transaction-only handles are\n ambiguous and fail closed before provisioning writes. Caller-owned\n transactions never run `_smrt_backfills` DDL and require that table to\n already exist; use the root database when initialization or recovery is\n needed.\n\n## Gotchas\n\n- **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)\n- **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation\n- **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)\n- **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.\n- **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)\n"
|
|
4666
|
+
"agentDoc": "# @happyvertical/smrt-users\n\nMulti-tenant user management with RBAC, hierarchical tenants, session handling, and SvelteKit integration.\n\n## Modules\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `src/retention.ts` | expired session/token/CLI-auth reaping and its wiring into the framework retention sweep | [agents/retention.md](agents/retention.md) |\n\n## Models (14)\n\n| Model | Key Pattern |\n|-------|-------------|\n| User | Auth identity. `profileId` is a unique cross-package reference to smrt-profiles (one User per non-null Profile). Email auto-lowercased; readonly nullable unique `emailKey` is derived on save for durable normalized uniqueness. |\n| AccessRequest | \"Request access / waitlist\" record captured before a `User` exists. CLOSED generated surface (`api`/`mcp`/`cli` = `[]`) — all access via `AccessRequestService`. Email normalized + indexed; JSON `requestContext` (NOT `context` — reserved for slug scoping). |\n| Tenant | **STI** + hierarchical parent-child. `hierarchyPath` (materialized path), `hierarchyLevel`. Max depth 10. |\n| Session | Server-side. Secure UUID. TTL in **seconds** (not ms). Status auto-updates to EXPIRED on access. |\n| MagicLinkToken | Single-use email login token. Backed by `MagicLinkService`. |\n| Role | `tenantId = null` → system role (available to all tenants). `isSystem: true` blocks deletion. `inheritsToDescendants: true` (default false) opts the role's membership authority into descendant tenants. |\n| Permission | Slug format: `resource.action`. Parsed by PermissionResolver. |\n| Membership | User + Tenant + Role junction. UNIQUE(userId, tenantId). |\n| Group | Team within a tenant. Multiple roles via GroupRole. |\n| GroupMember, GroupRole, RolePermission | Join tables. |\n| MembershipOverride | Per-user permission grant/deny. **DENY always wins.** |\n| TenantPermissionOverride | Tenant-level cascade overrides. Effect: INHERIT/GRANT/DENY. |\n\n## Permission Resolution — Precedence (broad → specific, most-specific wins)\n\n`PermissionResolver.resolvePermissions` builds the effective set in this order;\neach later layer overrides earlier ones:\n\n1. **Tenant-inherited** — walk ancestors, apply each `TenantPermissionOverride`\n down the cascade (GRANT adds, DENY removes within the hierarchy)\n2. **Membership role** — base permissions from the user's role in the tenant\n3. **Group roles** — permissions from all groups the user belongs to **in that tenant**\n4. **Tenant-level DENY** *(removes; overrides role/group grants, tenant-wide)* — a\n `TenantPermissionOverride` with effect `DENY` is a HARD, tenant-wide block: it\n subtracts the DENY'd slug even if a role or group granted it (steps 2–3). It\n sits just **above** the per-user membership overrides and **below** role/group.\n5. **Membership GRANT override** *(re-adds; most specific)* — a per-user GRANT can\n re-add a slug a tenant DENY'd in step 4, because it is more specific.\n6. **Membership DENY override** *(absolute; always wins)* — a per-user DENY removes\n the slug last and is never overridden.\n\nSo a permission a role grants but the tenant DENYs is **removed**, unless that\nexact user also has a membership-GRANT override for it. A membership-DENY always\nwins. Tenant-DENY of an inherited/cascade grant still blocks it (unchanged).\nThe hard block reflects the tenant cascade's **net** resolution, not an\nunconditional union of every DENY in the chain — so a more-specific tenant GRANT\n(e.g. a child sub-tenant re-granting a permission its parent DENYs) still wins.\n\n### Membership selection — hierarchical inheritance (opt-in, #1866)\n\nWhich membership feeds step 2 above:\n\n1. **A direct membership row in the target tenant always pins resolution to\n itself** — active rows resolve normally; an inactive (pending/suspended)\n row resolves to the **empty set**. A direct row therefore *attenuates*\n rather than unions: \"viewer here, despite being network admin\" gives\n viewer, and a suspension in the child is effective even for users holding\n inheritable authority on an ancestor.\n2. **No direct row** → the resolver walks the tenant's ancestors (nearest\n first, from `hierarchyPath`) and resolves through the **nearest ACTIVE\n ancestor membership whose role has `inheritsToDescendants: true`**.\n Unflagged or inactive ancestor memberships are skipped (they neither confer\n nor block); there is **no union across the chain** — the nearest flagged\n membership alone is used. To attenuate a specific descendant, create a\n direct membership there or use a tenant-level DENY.\n3. **No qualifying ancestor** → empty set (byte-identical to the\n pre-inheritance resolver; with no role flagged, nothing changes).\n\nAll later layers run unchanged against the **target** tenant: the tenant\ncascade and tenant-DENY hard block come from the target tenant (a child can\ncarve authority out of an inherited role), group roles stay exact-tenant (only\ntarget-tenant groups contribute; ancestor groups never flow down), and\nmembership GRANT/DENY overrides travel with the ancestor membership used.\n`PermissionResolutionResult.inheritedFromTenantId` reports the ancestor tenant\nwhen inheritance was used (`null` for direct resolution).\n\nSafety: resolution is bounded by `MAX_TENANT_HIERARCHY_DEPTH`; malformed\n`hierarchyPath` values fail closed to the empty set — too deep,\nself-referential, duplicate ancestors, or inconsistent with the actual\n`parentTenantId` chain (the path is verified link-by-link against the loaded\nancestor rows before it is trusted as an authorization source). Tenant `status` is not consulted (parity with direct\nresolution). Caching: a long-lived `(user, tenant)` permission cache must also\ninvalidate on ancestor-membership changes and on `Role.inheritsToDescendants`\nflips — request-scoped caches (the common pattern) are unaffected.\n\nFlag roles at seed time with\n`seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] })` (additive:\nlisted slugs are flagged, omitted slugs are never unflagged; unknown slugs\nthrow). The default seed leaves every role exact-tenant.\n\n## Operation Permission Guards\n\nSMRT derives a fine-grained, per-`<collection>.<action>` permission catalog **from the\nmanifest** (`PermissionCatalogService`, including custom `@smrt` actions — not just CRUD)\nand enforces it two ways: app-side via `assertOperationPermission()` / `PermissionResolver`,\nand at the database via generated **Postgres RLS** policies\n(`generatePostgresPermissionSql()` / `applyPostgresPermissionPolicies()`) that read the\nsession-injected `smrt.permissions` / `smrt.tenant_id` (set by `withSessionPermissionContext`\nvia `set_config`). Because the RLS teeth are at the data layer, enforcement is door-agnostic\n— REST, MCP, and in-process callers are all bounded once the principal's context is set.\nSetup: README → *Manifest-derived permission catalog* and *Postgres RLS enforcement*.\n\n- Use `assertOperationPermission()` for hand-written mutations in SvelteKit form\n actions, custom endpoints, CLI scripts, and jobs. It derives the same\n `<collection>.<action>` slugs as `PermissionCatalogService` (`list`/`get` →\n `read`), requires the slug to exist in the catalog, then resolves permissions\n through `PermissionResolver`.\n- `assertOperationPermission()` throws fail-closed by default. Use\n `{ onDeny: 'return' }`, `checkOperationPermission()`, or\n `hasOperationPermission()` when a structured/boolean result is needed.\n- **Resource-tenant calling convention**: for resource-anchored authorization,\n pass the **resource's** tenant id — `tenantId: resourceTenantId` — not the\n session's current tenant. With `Role.inheritsToDescendants` flagged, a\n root-tenant admin then passes for any descendant resource with no app-side\n authority logic (and no membership fan-out), while per-child delegation and\n DENY precedence keep working. Do NOT pass a session-scoped `membership`\n alongside a different resource `tenantId` — a membership/tenant mismatch\n fails closed by design; omit `membership` and let the resolver look it up.\n- System context and super-admin bypass context are honored for parity with\n Postgres RLS. Pass `{ allowSuperAdminBypass: false }` on money-class or\n separation-of-duties operations that must require an explicit permission grant.\n- **Postgres RLS and membership inheritance**: RLS policies check the\n session-injected `smrt.permissions` list (resolved app-side by\n `PermissionResolver`), so a session whose `smrt.tenant_id` IS the child\n tenant gets inherited authority in RLS automatically. But RLS row filtering\n stays bound to the session's tenant setting — a root-tenant session acting\n on child-tenant rows is authorized by the app-level guard (resource-tenant\n convention above), not by RLS. This is a documented divergence, mirroring\n how #1829 handled bypass parity.\n- Seed role mappings with `RolePermissionCollection.seedRolePermissions()` or\n `RoleCollection.seedSystemRoles({ seedPermissions: true })`. The default\n matrix maps owner/admin to all catalog permissions, member to read/create for\n ordinary app resources, and viewer to read-only. Member create grants\n intentionally exclude users/RBAC authority and security resources (`users`,\n `tenants`, `roles`, `permissions`, memberships, groups, sessions, magic-link\n tokens, and related join/override tables). Re-seeding is additive and\n idempotent; pruning stale mappings requires `{ prune: true }`. When a\n contributing package registers a new built-in self-personalization\n permission after system roles already exist, call the explicit,\n idempotent `RolePermissionCollection.seedDefaultRolePersonalizationPermissions()`\n upgrade helper; it targets owner/admin/member/viewer only and never grants\n custom roles.\n\n**Critical**: `getGroupIdsForTenant(userId, tenantId)` (joins with groups table to scope by tenant). Never use `getGroupIds()` — it's cross-tenant.\n\n## Hierarchical Tenants\n\n- `TenantCollection.createChild()` auto-calculates hierarchy fields, enforces depth limit\n- `moveToParent()` updates tenant + ALL descendants' paths/levels\n- `cascadePermissions` (parent pushes down) + `inheritPermissions` (child accepts) — both must be true\n- `getTree(rootId?)` returns nested structure for UI\n- Two independent downward flows: the `TenantPermissionOverride` **cascade**\n (tenant-level permission config, flags above) and **membership-role\n inheritance** (`Role.inheritsToDescendants`, per-role opt-in — see\n \"Membership selection\" above). The cascade flags do not gate membership\n inheritance.\n\n## SvelteKit Integration\n\n```typescript\n// hooks.server.ts\nexport const handle = createSessionHandler({ db, ttl: 604800, skipPaths: ['/api/public'] });\n// Populates event.locals: { user, membership, permissions: string[], tenantId, sessionId }\n\n// +page.server.ts\nawait createSessionCookie(event, userId, tenantId, { db });\nawait destroySessionCookie(event, { db });\nawait switchSessionTenant(event, tenantId, { db });\n```\n\n## Mobile `/api/mobile` Handlers (ADR 0001 Phase 3.5, #1748)\n\n`createMobileAuthHandlers(options)` (from `/sveltekit`) returns the mountable\nserver side of the KMP mobile contract: `authStart` (`POST auth/start`,\nserver-brokered PKCE via `OidcLoginService`), `authComplete`\n(`POST auth/complete`, code + echoed `state`/`codeVerifier` → bearer\nsession), `session.GET`/`session.DELETE` (bootstrap/logout), and\n`guard`/`withSession` — the bearer middleware for app-owned mobile routes.\nCore logic lives in `MobileAuthService` (framework-agnostic; exported from\nthe package root).\n\n- **Bearer = session id** (same convention as `TerminalAuthService`); 401\n bodies are `{ error, code }` and drive the mobile client's re-auth flow.\n- **Stateless handshake**: the OAuth `state` is an HMAC-signed token\n (secret: `stateSecret` ?? provider `clientSecret`) carrying\n nonce/provider/createdAt — full ID-token nonce verification with no\n server-side pending state. The `codeVerifier` never enters a URL: it is\n client-held per the frozen contract.\n- **Wire DTOs** come from `@happyvertical/smrt-mobile-contract`\n (`MobileAuthStartRequest` etc.) — one owning package for the Kotlin,\n Swift, and TypeScript shapes (compile-checked descriptors + parity test).\n- **Tenant options** honor `Role.inheritsToDescendants` (#1867): direct\n ACTIVE memberships plus descendants of flagged memberships (nearest\n flagged ancestor labels the option; any direct row pins; inactive direct\n rows exclude). Session binding defaults to the first DIRECT tenant —\n override with `resolveTenantId`.\n- **Hooks**: `resolveUser` (invite-gating; default provisions via\n `getOrCreateFromOidc`), `resolveTenantId`, `buildExtras` (bootstrap\n `extras`; model JSON must use `toPublicJSON({ permissions })` — #1822).\n `buildExtras` is the only app-domain bootstrap extension point. Never emit\n app fields such as `dashboard` at the response top level: they are outside\n `MobileSessionBootstrap` and the Kotlin decoder ignores them.\n- **Guard** wraps `withSessionPermissionContext`, so\n `assertOperationPermission`, tenancy context, and Postgres RLS all see the\n bearer caller; `OperationPermissionError` maps to 403 with a\n machine-readable `reason`.\n- **Uploads**: `resolveMobileUploadDedupKey` + the documented contract in\n `docs/content/architecture/mobile-upload-contract.md` (`clientCaptureId`\n field, `Idempotency-Key` header fallback); domain ingestion stays\n app-side. Framework-model writes ride `sync/apply`, not this path.\n- Configure `redirectUris` in production — RFC 8252 scheme rules always\n apply, but the allow list is the defense against redirecting authorization\n responses to attacker-controlled URIs.\n\n## Security (S5 #1400)\n\n- **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**\n User, Tenant, Group, Membership, MembershipOverride, Role, Permission,\n RolePermission, GroupRole, GroupMember, and TenantPermissionOverride generate\n `list`/`get` only — `create`/`update`/`delete` are intentionally NOT\n generated. The merged `requireRouteAuth` gate (#1540) enforces *authentication*,\n not *authorization*, and these models are not `@TenantScoped`, so an\n auto-generated mutating route would let any authenticated user self-grant a\n role/permission, flip a tenant's cascade flags, or change another user's auth\n identity. Mutate them through the permission-gated services (`TenantService`,\n collection helpers) or consumer-owned, permission-checked handlers. A\n structural regression test (`security-audit-1400.test.ts`) enumerates the\n registry to assert no authority model exposes a mutating op. (`cli` stays\n enabled — local-operator surface, outside the network/agent threat model.)\n- **`switchTenant` is fail-closed AND rotates the session id.**\n `SessionService.switchTenant` / `switchSessionTenant` verify the session's user\n has an ACTIVE membership in the target tenant before any write (the tenant id\n is the isolation key for every `@TenantScoped` query). A non-member/unknown-\n session switch returns `{ switched: false, sessionId: null, ... }` and mutates\n nothing. On a successful switch into a NON-null tenant the session id is\n ROTATED: a fresh `Session` (new secure id, fresh TTL, same user, new tenant,\n device context carried over) is minted and the old session is REVOKED — so a\n captured pre-switch id immediately stops validating, shrinking the blast radius\n of a leaked id across a tenant boundary. `switchTenant` returns a\n `SwitchTenantResult` (`{ switched, sessionId, session, rotated }`); callers MUST\n persist the returned `sessionId`. `switchSessionTenant` does this for you by\n re-setting the session cookie (preserving httpOnly/secure/sameSite) to the new\n id. A `null` clear stays in place (no rotation, no cookie change). The\n low-level `SessionCollection.setSessionTenant` is the UNGUARDED primitive (used\n for the null-clear path) — never call it with an untrusted tenant id.\n- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`\n refuses to provision a user when the IdP explicitly returns\n `email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An\n absent claim makes no assertion and is not enforced.\n- **RFC 9207 response issuer is exact.** OIDC callbacks validate a supplied\n `iss` against discovered metadata with exact string comparison before trusting\n a code or error. If metadata advertises issuer-response support, `iss` is\n required.\n- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios\n live in\n `packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both\n package suites execute that matrix and public docs reference it rather than\n maintaining another behavioral table. Default provisioning reuses only one\n unowned, global `Person`. Tenant-scoped, non-Person, duplicate-email,\n and already-owned matches fail before User/session creation. An existing\n issuer/subject link without a User must still be the unique global Person for\n the current verified claim email; once owned, the stable issuer/subject link\n reuses its canonical Person and owner.\n Issuer and subject are opaque, case-sensitive identifiers; preserve their\n exact value and use trim only to reject blank claims.\n- **`resolveProfile` is the application reconciliation boundary.** The\n SvelteKit handlers, `OidcLoginService`, and `getOrCreateFromOidc` accept the\n same hook inside the provisioning transaction. The service/handler path\n supplies protocol-validated claims; direct collection callers must validate\n and trust their claim source before calling `getOrCreateFromOidc`.\n Token/userinfo merging keeps `email` and `email_verified` paired to the same\n claim source; verification is never borrowed across sources.\n Resolver reads/writes use the supplied `db`, and the hook must be idempotent\n because a concurrent unique-key conflict can retry it. `undefined` chooses\n the secure default and `null` rejects, including exact issuer/subject reuse.\n For a new identity, a supplied Profile is still validated as the unique,\n unowned global Person for the verified email. For an exact existing identity,\n it must be the already-linked Profile and cannot rebind identity authority;\n stable-link owner and canonical-Person checks still apply. The hook receives a\n separate frozen claims snapshot; internal retry and persistence state is not\n exposed for mutation.\n- **Owned first binding requires `authorizeProfileOwner`.** An invitation or\n approval workflow may explicitly return both its pre-provisioned canonical\n global `Person` and existing approved `User` from this transaction-bound\n hook. `undefined` keeps the secure `profile_owned` default and `null`\n rejects. SMRT treats only the selected IDs as input, reloads them in the\n provisioning transaction, and requires `email_verified === true`, the unique\n canonical global Person for the normalized claim email, exactly one owner,\n that owner as the selected User, and the same normalized User email. The\n hook runs before identity/User/session creation and may be retried, so use\n only its supplied `db` and `users` handles and keep application authorization\n idempotent. Never authorize from email matching alone. Existing exact\n identities cannot be rebound; when `resolveProfile` is also present both\n hooks must select the same Profile.\n- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one\n transaction. The database arbiters are `OidcIdentity.identityKey`,\n private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the\n unique `User.profileId`; local callbacks acquire exact issuer/subject and normalized\n email locks in deterministic order so changed email claims also serialize.\n SQLite and DuckDB callbacks additionally serialize every root-handle statement\n the coordinator owns per database URL — `_smrt_backfills` initialization, the\n transaction, and the post-commit rebind — because one adapter multiplexes a\n single native connection and cannot safely overlap unrelated root\n transactions. Never overlap two statements on one such handle, inside a\n transaction or not: rebind and owner/email candidate reads are sequential,\n never `Promise.all`. Owner-authorized DuckDB callbacks use that same\n root-handle serialization;\n PostgreSQL deadlock and serialization errors use a bounded transaction retry.\n Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP\n display names cannot trigger a natural-key upsert;\n run\n `smrt db:status`, `smrt db:migrate`, then `smrt db:status` before deployment.\n Stop or upgrade old writers first. Before migration, group\n non-null `users.profile_id` values, then reconcile duplicates. After\n migration, run public `backfillProfileEmailKeys(db)` followed by\n `backfillUserEmailKeys(db)` from one deploy process. Both use the shared\n TypeScript `normalizeIdentityEmail()` implementation transactionally and are\n idempotent; the User backfill fails before writes if normalized duplicates\n remain. Every OIDC path requires the Profile email-key readiness marker;\n creating a User or checking User email uniqueness additionally requires the\n User marker. A stable issuer/subject with an existing owning User skips only\n the User email-key lookup and marker. Full scans remain in the explicit deploy\n step; guarded runtime paths use indexed keys and validate only returned\n candidates. Multiple null links remain valid.\n Legacy race keys backfill only after canonical validation. Pass a root\n database on adapters such as DuckDB that cannot create nested savepoints.\n Root adapters must expose `beginTransaction`; transaction-only handles are\n ambiguous and fail closed before provisioning writes. Caller-owned\n transactions never run `_smrt_backfills` DDL and require that table to\n already exist; use the root database when initialization or recovery is\n needed.\n\n## Gotchas\n\n- **seedSystemRoles() required**: call `RoleCollection.seedSystemRoles()` at app init (creates owner/admin/member/viewer)\n- **PermissionResolver casts `as any`**: collections have protected constructors — known framework limitation\n- **Session TTL in seconds**: `DEFAULT_SESSION_TTL = 7 * 24 * 60 * 60` (not milliseconds)\n- **Users are cross-tenant**: one user, many tenants via Membership. Email globally unique.\n- **Batch permission queries**: resolver fetches all permission IDs in one query, then maps to slugs (avoids N+1)\n",
|
|
4667
|
+
"moduleDocs": [
|
|
4668
|
+
{
|
|
4669
|
+
"path": "agents/retention.md",
|
|
4670
|
+
"module": "retention",
|
|
4671
|
+
"content": "# smrt-users/credential retention\n\nModule semantics for `src/retention.ts` and the three `deleteExpired()` paths\nit drives. Package orientation, the permission model, and the security rules\nthat apply before editing anything live in [../AGENTS.md](../AGENTS.md) — read\nthat first.\n\n## Credential retention (#2375)\n\n`SessionCollection.deleteExpired()`,\n`UsersMagicLinkTokenCollection.deleteExpired()` and\n`UsersCliAuthRequestCollection.deleteExpired()` all existed, and all waited for\nan application to remember to call them. Expired credential rows are the worst\nkind of unbounded growth: worthless the moment they expire, and exactly the\nrows an attacker would like to still find in the table.\n\n`src/retention.ts` contributes the three to the framework retention sweep in\n`@happyvertical/smrt-core`, so `smrt db:prune` and a running jobs `TaskRunner`\nreap them.\n\n- **The entry point registers on import.** `src/index.ts` calls\n `registerUserRetentionTasks()`, so any process that loaded this package\n contributes the tasks — including `smrt db:prune`, which imports the package\n optionally for exactly that reason. Registering is not scheduling: nothing is\n deleted until something runs a sweep.\n- **Task names** are `users-sessions`, `users-magic-link-tokens`,\n `users-cli-auth-requests` — prefixed with the package's short name because\n the retention registry is one process-global namespace. Opt one out with\n `runRetentionSweep(db, { tasks: { 'users-sessions': false } })` or\n `smrt db:prune --skip users-sessions`; `unregisterUserRetentionTasks()`\n removes all three.\n- **There is no retention window to configure.** An expired credential has\n nothing worth retaining, so each task deletes only already-expired rows. An\n application that keeps expired sessions for audit should opt the task out and\n archive them itself.\n- **All three are a single counted DELETE**, not a hydrate-and-delete loop.\n They now run unattended on a timer, and a per-row delete that throws part-way\n leaves the rest of the expired rows un-reaped — the #1400 reasoning that\n already applied to sessions, now applied to the other two. Counting first\n also gives a usable figure where `rowCount` is not populated, and is what\n makes `{ dryRun }` preview the same predicate rather than an estimate. The\n count and the delete are not one transaction, so the figure is approximate\n under concurrent writers.\n- **`expiresAt` is `@field({ indexed: true })`** on `Session`,\n `UsersMagicLinkToken` and `UsersCliAuthRequest`: the prune predicate scans\n that column on every pass.\n"
|
|
4672
|
+
}
|
|
4673
|
+
]
|
|
4657
4674
|
}
|
package/dist/sveltekit.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-
|
|
1
|
+
import { C as decodeOidcTransaction, E as resolveOidcProviderConfig, S as OidcLoginService, T as getUsersOidcConfig, _ as readMobileBearerToken, a as TerminalAuthRateLimitError, g as MobileAuthService, h as MobileAuthError, i as TerminalAuthError, m as withSessionPermissionContext, o as TerminalAuthService, s as OperationPermissionError, v as validateMobileRedirectUri, w as encodeOidcTransaction, x as OidcLoginError, y as SessionService } from "./chunks/TerminalAuthService-BfX2Ujqe.js";
|
|
2
2
|
import { DiscoveryArtifactValidationError, SMRT_APP_RESULT_CONTRACT, SMRT_APP_RESULT_SCHEMA, SMRT_APP_RESULT_VERSION, SMRT_DISCOVERY_CONFORMANCE_ARTIFACT_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_SCHEMA, SMRT_DISCOVERY_CONFORMANCE_VERSION, SMRT_MCP_RESULT_METADATA_KEY, canonicalizeDiscoveryArtifact, createDiscoveryConformanceArtifact, deriveCommandRequirements, validateDiscoveryConformanceArtifact } from "./app-contract.js";
|
|
3
3
|
import { ObjectRegistry } from "@happyvertical/smrt-core";
|
|
4
4
|
import { createLogger } from "@happyvertical/logger";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-users",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Multi-tenant user management for the SMRT framework - users, tenants, roles, permissions, groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"smrtRawPrimitives": "strict",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"@happyvertical/logger": "^0.88.0",
|
|
49
49
|
"@happyvertical/sql": "^0.88.0",
|
|
50
50
|
"jose": "^6.2.3",
|
|
51
|
-
"@happyvertical/smrt-config": "0.
|
|
52
|
-
"@happyvertical/smrt-core": "0.
|
|
53
|
-
"@happyvertical/smrt-
|
|
54
|
-
"@happyvertical/smrt-tenancy": "0.
|
|
55
|
-
"@happyvertical/smrt-
|
|
56
|
-
"@happyvertical/smrt-
|
|
57
|
-
"@happyvertical/smrt-ui": "0.
|
|
51
|
+
"@happyvertical/smrt-config": "0.42.0",
|
|
52
|
+
"@happyvertical/smrt-core": "0.42.0",
|
|
53
|
+
"@happyvertical/smrt-profiles": "0.42.0",
|
|
54
|
+
"@happyvertical/smrt-tenancy": "0.42.0",
|
|
55
|
+
"@happyvertical/smrt-mobile-contract": "0.42.0",
|
|
56
|
+
"@happyvertical/smrt-types": "0.42.0",
|
|
57
|
+
"@happyvertical/smrt-ui": "0.42.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@sveltejs/package": "^2.5.8",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"typescript": "5.9.3",
|
|
67
67
|
"vite": "8.1.4",
|
|
68
68
|
"vitest": "4.1.10",
|
|
69
|
-
"@happyvertical/smrt-vitest": "0.
|
|
69
|
+
"@happyvertical/smrt-vitest": "0.42.0"
|
|
70
70
|
},
|
|
71
71
|
"keywords": [
|
|
72
72
|
"smrt",
|