@venturekit/auth 0.0.0-dev.20260701113915 → 0.0.0-dev.20260704225856
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/vk_auth_003_role_scopes.sql +43 -0
- package/dist/roles/index.d.ts +5 -1
- package/dist/roles/index.d.ts.map +1 -1
- package/dist/roles/index.js +4 -1
- package/dist/roles/index.js.map +1 -1
- package/dist/roles/role-scopes.d.ts +92 -0
- package/dist/roles/role-scopes.d.ts.map +1 -0
- package/dist/roles/role-scopes.js +122 -0
- package/dist/roles/role-scopes.js.map +1 -0
- package/dist/server/cookies.d.ts +77 -6
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +55 -13
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/federated-routes.d.ts +29 -22
- package/dist/server/federated-routes.d.ts.map +1 -1
- package/dist/server/federated-routes.js +31 -4
- package/dist/server/federated-routes.js.map +1 -1
- package/dist/server/federated.d.ts.map +1 -1
- package/dist/server/federated.js +7 -11
- package/dist/server/federated.js.map +1 -1
- package/dist/server/handoff-routes.d.ts +130 -0
- package/dist/server/handoff-routes.d.ts.map +1 -0
- package/dist/server/handoff-routes.js +178 -0
- package/dist/server/handoff-routes.js.map +1 -0
- package/dist/server/handoff.d.ts +112 -0
- package/dist/server/handoff.d.ts.map +1 -0
- package/dist/server/handoff.js +102 -0
- package/dist/server/handoff.js.map +1 -0
- package/dist/server/index.d.ts +10 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/passwordless.d.ts +68 -0
- package/dist/server/passwordless.d.ts.map +1 -0
- package/dist/server/passwordless.js +136 -0
- package/dist/server/passwordless.js.map +1 -0
- package/dist/server/revoke.d.ts +10 -0
- package/dist/server/revoke.d.ts.map +1 -1
- package/dist/server/revoke.js +19 -1
- package/dist/server/revoke.js.map +1 -1
- package/dist/server/store/postgres.d.ts +35 -0
- package/dist/server/store/postgres.d.ts.map +1 -0
- package/dist/server/store/postgres.js +88 -0
- package/dist/server/store/postgres.js.map +1 -0
- package/dist/server/token-utils.d.ts +12 -2
- package/dist/server/token-utils.d.ts.map +1 -1
- package/dist/server/token-utils.js +9 -4
- package/dist/server/token-utils.js.map +1 -1
- package/package.json +13 -4
- package/src/migrations/vk_auth_003_role_scopes.sql +43 -0
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
export * from './types/index.js';
|
|
7
7
|
export { createCognitoConfig, DEFAULT_COGNITO_CONFIG, buildUserPoolConfig, } from './cognito/index.js';
|
|
8
8
|
export type { UserPoolOutputs, UserPoolInfraConfig } from './cognito/user-pool.js';
|
|
9
|
-
export { hasScope, hasAnyScope, hasAllScopes, getScopesForRoles, validateRolesConfig, } from './roles/index.js';
|
|
9
|
+
export { hasScope, hasAnyScope, hasAllScopes, getScopesForRoles, validateRolesConfig, listRoleScopes, getRoleScopes, setRoleScopes, grantScopeToRole, revokeScopeFromRole, createRoleScopesResolver, } from './roles/index.js';
|
|
10
|
+
export type { Querier, RoleScopeRow, RoleScopesLookup, RoleScopesResolver, RoleScopesResolverOptions, } from './roles/index.js';
|
|
10
11
|
export { decodeTokenUnsafe, decodeToken, // deprecated alias of decodeTokenUnsafe
|
|
11
12
|
verifyAndDecode, extractUserFromToken, isTokenExpired, getTokenExpiry, } from './session/index.js';
|
|
12
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAGnF,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,EAInB,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,WAAW,EAAE,wCAAwC;AACrD,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,cAAc,GACf,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,11 @@ export * from './types/index.js';
|
|
|
8
8
|
// Cognito
|
|
9
9
|
export { createCognitoConfig, DEFAULT_COGNITO_CONFIG, buildUserPoolConfig, } from './cognito/index.js';
|
|
10
10
|
// Roles
|
|
11
|
-
export { hasScope, hasAnyScope, hasAllScopes, getScopesForRoles, validateRolesConfig,
|
|
11
|
+
export { hasScope, hasAnyScope, hasAllScopes, getScopesForRoles, validateRolesConfig,
|
|
12
|
+
// DB-backed role → scopes mapping (vk_role_scopes: structure ships
|
|
13
|
+
// with this package's migrations, rows owned by the app; cached
|
|
14
|
+
// resolver for scope-granting middleware + CRUD for management UIs)
|
|
15
|
+
listRoleScopes, getRoleScopes, setRoleScopes, grantScopeToRole, revokeScopeFromRole, createRoleScopesResolver, } from './roles/index.js';
|
|
12
16
|
// Session
|
|
13
17
|
export { decodeTokenUnsafe, decodeToken, // deprecated alias of decodeTokenUnsafe
|
|
14
18
|
verifyAndDecode, extractUserFromToken, isTokenExpired, getTokenExpiry, } from './session/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,QAAQ;AACR,cAAc,kBAAkB,CAAC;AAEjC,UAAU;AACV,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,QAAQ;AACR,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,QAAQ;AACR,cAAc,kBAAkB,CAAC;AAEjC,UAAU;AACV,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,QAAQ;AACR,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,mBAAmB;AACnB,mEAAmE;AACnE,gEAAgE;AAChE,oEAAoE;AACpE,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAS1B,UAAU;AACV,OAAO,EACL,iBAAiB,EACjB,WAAW,EAAE,wCAAwC;AACrD,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,cAAc,GACf,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- @venturekit/auth — role → scopes mapping.
|
|
2
|
+
--
|
|
3
|
+
-- The DB is the SOURCE OF TRUTH for which scopes each role grants.
|
|
4
|
+
-- VentureKit ships the STRUCTURE (this migration, applied automatically
|
|
5
|
+
-- with every other package migration); the consuming app ships DATA
|
|
6
|
+
-- migrations to seed its own taxonomy, e.g.:
|
|
7
|
+
--
|
|
8
|
+
-- INSERT INTO vk_role_scopes (role, scope) VALUES
|
|
9
|
+
-- ('member', 'member.verified'),
|
|
10
|
+
-- ('moderator', 'member.verified'),
|
|
11
|
+
-- ('moderator', 'moderation.reports.read')
|
|
12
|
+
-- ON CONFLICT DO NOTHING;
|
|
13
|
+
--
|
|
14
|
+
-- Lives in @venturekit/auth — NOT in a pro package — because a role →
|
|
15
|
+
-- scopes matrix is baseline authorization for ANY app: single-tenant
|
|
16
|
+
-- apps key it by their global user role; multi-tenant apps feed it to
|
|
17
|
+
-- `@venturekit-pro/tenancy`'s scopes middleware
|
|
18
|
+
-- (`createTenantUserScopesMiddleware({ scopesByRole: resolver.lookup })`).
|
|
19
|
+
--
|
|
20
|
+
-- At runtime the mapping is read through `createRoleScopesResolver()`
|
|
21
|
+
-- (cached, TTL-refreshed). Mutations go through `setRoleScopes` /
|
|
22
|
+
-- `grantScopeToRole` / `revokeScopeFromRole`, so apps can build
|
|
23
|
+
-- management UIs on top — changes propagate to warm instances within
|
|
24
|
+
-- the resolver's TTL.
|
|
25
|
+
--
|
|
26
|
+
-- Deliberately GLOBAL (no tenant column): the mapping is the app's
|
|
27
|
+
-- authorization POLICY, not tenant data. Per-tenant overrides, if ever
|
|
28
|
+
-- needed, belong in a separate overlay table so the hot path stays a
|
|
29
|
+
-- single cached full-table read.
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS vk_role_scopes (
|
|
32
|
+
role text NOT NULL,
|
|
33
|
+
scope text NOT NULL,
|
|
34
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
35
|
+
PRIMARY KEY (role, scope)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
COMMENT ON TABLE vk_role_scopes IS
|
|
39
|
+
'Role → scope grants. Structure owned by @venturekit/auth; rows owned by the consuming app (seed via app migrations, manage via the role-scopes helpers).';
|
|
40
|
+
|
|
41
|
+
-- Scope grants are read by role on every request that misses the
|
|
42
|
+
-- resolver cache; the PK already serves (role, scope) lookups and
|
|
43
|
+
-- role-prefix scans.
|
package/dist/roles/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Roles
|
|
3
3
|
*
|
|
4
|
-
* Utilities for managing roles and scopes
|
|
4
|
+
* Utilities for managing roles and scopes — the static config helpers
|
|
5
|
+
* below, plus the DB-backed `vk_role_scopes` mapping (CRUD + cached
|
|
6
|
+
* resolver) in ./role-scopes.ts.
|
|
5
7
|
*/
|
|
8
|
+
export { listRoleScopes, getRoleScopes, setRoleScopes, grantScopeToRole, revokeScopeFromRole, createRoleScopesResolver, } from './role-scopes.js';
|
|
9
|
+
export type { Querier, RoleScopeRow, RoleScopesLookup, RoleScopesResolver, RoleScopesResolverOptions, } from './role-scopes.js';
|
|
6
10
|
import type { RolesConfig } from '../types/index.js';
|
|
7
11
|
/**
|
|
8
12
|
* Check if user roles grant a specific scope
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAQ,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;GAEG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,WAAW,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EAAE,EACnB,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAET;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EAAE,EACnB,MAAM,EAAE,MAAM,EAAE,EAChB,MAAM,EAAE,WAAW,GAClB,OAAO,CAET;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EAAE,EACnB,MAAM,EAAE,WAAW,GAClB,MAAM,EAAE,CAWV;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,EAAE,CAejE"}
|
package/dist/roles/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Roles
|
|
3
3
|
*
|
|
4
|
-
* Utilities for managing roles and scopes
|
|
4
|
+
* Utilities for managing roles and scopes — the static config helpers
|
|
5
|
+
* below, plus the DB-backed `vk_role_scopes` mapping (CRUD + cached
|
|
6
|
+
* resolver) in ./role-scopes.ts.
|
|
5
7
|
*/
|
|
8
|
+
export { listRoleScopes, getRoleScopes, setRoleScopes, grantScopeToRole, revokeScopeFromRole, createRoleScopesResolver, } from './role-scopes.js';
|
|
6
9
|
/**
|
|
7
10
|
* Check if user roles grant a specific scope
|
|
8
11
|
*/
|
package/dist/roles/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,cAAc,EACd,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAW1B;;GAEG;AACH,MAAM,UAAU,QAAQ,CACtB,SAAmB,EACnB,KAAa,EACb,MAAmB;IAEnB,6BAA6B;IAC7B,IAAI,MAAM,CAAC,cAAc,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,iBAAiB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACxD,OAAO,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CACzB,SAAmB,EACnB,MAAgB,EAChB,MAAmB;IAEnB,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAmB,EACnB,MAAgB,EAChB,MAAmB;IAEnB,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,SAAmB,EACnB,MAAmB;IAEnB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IAEjC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QACjE,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAmB;IACrD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAO,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEjE,4BAA4B;IAC5B,IAAI,MAAM,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7D,MAAM,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,WAAW,kBAAkB,CAAC,CAAC;IACrE,CAAC;IAED,gCAAgC;IAChC,IAAI,MAAM,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;QACnE,MAAM,CAAC,IAAI,CAAC,qBAAqB,MAAM,CAAC,cAAc,kBAAkB,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DB-backed role → scopes mapping over `vk_role_scopes`.
|
|
3
|
+
*
|
|
4
|
+
* The table structure ships with this package
|
|
5
|
+
* (`migrations/vk_auth_003_role_scopes.sql`); the ROWS are the
|
|
6
|
+
* consuming app's authorization policy — seeded by an app data
|
|
7
|
+
* migration and mutable at runtime through the helpers here, so apps
|
|
8
|
+
* can put a management UI on top without redeploying to change what a
|
|
9
|
+
* role may do.
|
|
10
|
+
*
|
|
11
|
+
* Sits in `@venturekit/auth` (not a pro package) because a role →
|
|
12
|
+
* scopes matrix is baseline authorization for ANY app:
|
|
13
|
+
* - single-tenant apps resolve their global user role through
|
|
14
|
+
* {@link createRoleScopesResolver} and grant the result;
|
|
15
|
+
* - multi-tenant apps pass `resolver.lookup` straight to
|
|
16
|
+
* `@venturekit-pro/tenancy`'s
|
|
17
|
+
* `createTenantUserScopesMiddleware({ scopesByRole })`.
|
|
18
|
+
*
|
|
19
|
+
* Reading on the hot path goes through the resolver: one full-table
|
|
20
|
+
* read, cached per process with a TTL (default 30s).
|
|
21
|
+
*
|
|
22
|
+
* **Consistency model.** Mutations are visible to the mutating
|
|
23
|
+
* instance immediately when it calls `resolver.invalidate()`, and to
|
|
24
|
+
* every other warm instance within one TTL — scope-mapping changes
|
|
25
|
+
* are rare and tolerate seconds of convergence.
|
|
26
|
+
*
|
|
27
|
+
* All helpers take a caller-supplied {@link Querier} — no hard
|
|
28
|
+
* dependency on `@venturekit/data`.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Minimal query interface — same shape as `@venturekit/data`'s `query`
|
|
32
|
+
* and the Queriers used across the workspace. Declared locally so this
|
|
33
|
+
* package carries no data-layer dependency.
|
|
34
|
+
*/
|
|
35
|
+
export type Querier = <T = Record<string, unknown>[]>(sql: string, params?: unknown[]) => Promise<T>;
|
|
36
|
+
/** One `vk_role_scopes` row. */
|
|
37
|
+
export interface RoleScopeRow {
|
|
38
|
+
role: string;
|
|
39
|
+
scope: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Lookup signature scope-granting middleware consumes. Returning `[]`
|
|
43
|
+
* (unknown role) grants nothing — fail-closed.
|
|
44
|
+
*/
|
|
45
|
+
export type RoleScopesLookup = (role: string) => Promise<readonly string[]> | readonly string[];
|
|
46
|
+
/** The full mapping, `{ role: [scope, …] }`, scopes sorted. */
|
|
47
|
+
export declare function listRoleScopes(querier: Querier): Promise<Record<string, string[]>>;
|
|
48
|
+
/** Scopes granted by one role (empty array when the role is unknown). */
|
|
49
|
+
export declare function getRoleScopes(querier: Querier, role: string): Promise<string[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Replace a role's scope set atomically (single statement — a request
|
|
52
|
+
* racing the change sees either the old or the new complete set,
|
|
53
|
+
* never a partial one). An empty `scopes` array removes the role.
|
|
54
|
+
*/
|
|
55
|
+
export declare function setRoleScopes(querier: Querier, role: string, scopes: readonly string[]): Promise<void>;
|
|
56
|
+
/** Add one grant. Idempotent. */
|
|
57
|
+
export declare function grantScopeToRole(querier: Querier, role: string, scope: string): Promise<void>;
|
|
58
|
+
/** Remove one grant. Idempotent. */
|
|
59
|
+
export declare function revokeScopeFromRole(querier: Querier, role: string, scope: string): Promise<void>;
|
|
60
|
+
export interface RoleScopesResolverOptions {
|
|
61
|
+
/** How the resolver reaches the DB. */
|
|
62
|
+
querier: Querier;
|
|
63
|
+
/**
|
|
64
|
+
* Cache lifetime, ms. Default 30 000. Governs how fast a mapping
|
|
65
|
+
* change propagates to OTHER warm instances (the mutating instance
|
|
66
|
+
* calls `invalidate()` and sees it immediately).
|
|
67
|
+
*/
|
|
68
|
+
ttlMs?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface RoleScopesResolver {
|
|
71
|
+
/**
|
|
72
|
+
* `(role) => scopes`. Pass as `scopesByRole` to tenancy's
|
|
73
|
+
* `createTenantUserScopesMiddleware`, or call directly from custom
|
|
74
|
+
* scope-granting middleware.
|
|
75
|
+
*/
|
|
76
|
+
lookup: RoleScopesLookup;
|
|
77
|
+
/** Drop the cache — the next lookup reloads from the DB. */
|
|
78
|
+
invalidate(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build the cached mapping resolver.
|
|
82
|
+
*
|
|
83
|
+
* Failure semantics:
|
|
84
|
+
* - load fails and a previous snapshot exists → serve the stale
|
|
85
|
+
* snapshot (authorization availability beats freshness; the next
|
|
86
|
+
* lookup retries the load);
|
|
87
|
+
* - load fails with NO snapshot (cold start during a DB outage) →
|
|
88
|
+
* the error propagates, failing the request loudly rather than
|
|
89
|
+
* silently granting nothing to everyone.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createRoleScopesResolver(options: RoleScopesResolverOptions): RoleScopesResolver;
|
|
92
|
+
//# sourceMappingURL=role-scopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-scopes.d.ts","sourceRoot":"","sources":["../../src/roles/role-scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAClD,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB,gCAAgC;AAChC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,MAAM,KACT,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,SAAS,MAAM,EAAE,CAAC;AAIpD,+DAA+D;AAC/D,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CASnC;AAED,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,EAAE,CAAC,CAMnB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,iCAAiC;AACjC,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,oCAAoC;AACpC,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAKf;AAID,MAAM,WAAW,yBAAyB;IACxC,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,EAAE,gBAAgB,CAAC;IACzB,4DAA4D;IAC5D,UAAU,IAAI,IAAI,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GACjC,kBAAkB,CAyCpB"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DB-backed role → scopes mapping over `vk_role_scopes`.
|
|
3
|
+
*
|
|
4
|
+
* The table structure ships with this package
|
|
5
|
+
* (`migrations/vk_auth_003_role_scopes.sql`); the ROWS are the
|
|
6
|
+
* consuming app's authorization policy — seeded by an app data
|
|
7
|
+
* migration and mutable at runtime through the helpers here, so apps
|
|
8
|
+
* can put a management UI on top without redeploying to change what a
|
|
9
|
+
* role may do.
|
|
10
|
+
*
|
|
11
|
+
* Sits in `@venturekit/auth` (not a pro package) because a role →
|
|
12
|
+
* scopes matrix is baseline authorization for ANY app:
|
|
13
|
+
* - single-tenant apps resolve their global user role through
|
|
14
|
+
* {@link createRoleScopesResolver} and grant the result;
|
|
15
|
+
* - multi-tenant apps pass `resolver.lookup` straight to
|
|
16
|
+
* `@venturekit-pro/tenancy`'s
|
|
17
|
+
* `createTenantUserScopesMiddleware({ scopesByRole })`.
|
|
18
|
+
*
|
|
19
|
+
* Reading on the hot path goes through the resolver: one full-table
|
|
20
|
+
* read, cached per process with a TTL (default 30s).
|
|
21
|
+
*
|
|
22
|
+
* **Consistency model.** Mutations are visible to the mutating
|
|
23
|
+
* instance immediately when it calls `resolver.invalidate()`, and to
|
|
24
|
+
* every other warm instance within one TTL — scope-mapping changes
|
|
25
|
+
* are rare and tolerate seconds of convergence.
|
|
26
|
+
*
|
|
27
|
+
* All helpers take a caller-supplied {@link Querier} — no hard
|
|
28
|
+
* dependency on `@venturekit/data`.
|
|
29
|
+
*/
|
|
30
|
+
/* ─── CRUD ──────────────────────────────────────────────────────── */
|
|
31
|
+
/** The full mapping, `{ role: [scope, …] }`, scopes sorted. */
|
|
32
|
+
export async function listRoleScopes(querier) {
|
|
33
|
+
const rows = await querier(`SELECT role, scope FROM vk_role_scopes ORDER BY role, scope`);
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const { role, scope } of rows) {
|
|
36
|
+
(out[role] ??= []).push(scope);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Scopes granted by one role (empty array when the role is unknown). */
|
|
41
|
+
export async function getRoleScopes(querier, role) {
|
|
42
|
+
const rows = await querier(`SELECT scope FROM vk_role_scopes WHERE role = $1 ORDER BY scope`, [role]);
|
|
43
|
+
return rows.map((r) => r.scope);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Replace a role's scope set atomically (single statement — a request
|
|
47
|
+
* racing the change sees either the old or the new complete set,
|
|
48
|
+
* never a partial one). An empty `scopes` array removes the role.
|
|
49
|
+
*/
|
|
50
|
+
export async function setRoleScopes(querier, role, scopes) {
|
|
51
|
+
await querier(`WITH removed AS (
|
|
52
|
+
DELETE FROM vk_role_scopes WHERE role = $1
|
|
53
|
+
)
|
|
54
|
+
INSERT INTO vk_role_scopes (role, scope)
|
|
55
|
+
SELECT $1, s FROM unnest($2::text[]) AS s
|
|
56
|
+
ON CONFLICT DO NOTHING`, [role, [...scopes]]);
|
|
57
|
+
}
|
|
58
|
+
/** Add one grant. Idempotent. */
|
|
59
|
+
export async function grantScopeToRole(querier, role, scope) {
|
|
60
|
+
await querier(`INSERT INTO vk_role_scopes (role, scope)
|
|
61
|
+
VALUES ($1, $2)
|
|
62
|
+
ON CONFLICT DO NOTHING`, [role, scope]);
|
|
63
|
+
}
|
|
64
|
+
/** Remove one grant. Idempotent. */
|
|
65
|
+
export async function revokeScopeFromRole(querier, role, scope) {
|
|
66
|
+
await querier(`DELETE FROM vk_role_scopes WHERE role = $1 AND scope = $2`, [
|
|
67
|
+
role,
|
|
68
|
+
scope,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the cached mapping resolver.
|
|
73
|
+
*
|
|
74
|
+
* Failure semantics:
|
|
75
|
+
* - load fails and a previous snapshot exists → serve the stale
|
|
76
|
+
* snapshot (authorization availability beats freshness; the next
|
|
77
|
+
* lookup retries the load);
|
|
78
|
+
* - load fails with NO snapshot (cold start during a DB outage) →
|
|
79
|
+
* the error propagates, failing the request loudly rather than
|
|
80
|
+
* silently granting nothing to everyone.
|
|
81
|
+
*/
|
|
82
|
+
export function createRoleScopesResolver(options) {
|
|
83
|
+
const ttlMs = options.ttlMs ?? 30_000;
|
|
84
|
+
let snapshot = null;
|
|
85
|
+
let loadedAt = 0;
|
|
86
|
+
let inflight = null;
|
|
87
|
+
const load = () => {
|
|
88
|
+
inflight ??= listRoleScopes(options.querier)
|
|
89
|
+
.then((mapping) => {
|
|
90
|
+
snapshot = mapping;
|
|
91
|
+
loadedAt = Date.now();
|
|
92
|
+
return mapping;
|
|
93
|
+
})
|
|
94
|
+
.finally(() => {
|
|
95
|
+
inflight = null;
|
|
96
|
+
});
|
|
97
|
+
return inflight;
|
|
98
|
+
};
|
|
99
|
+
const lookup = async (role) => {
|
|
100
|
+
if (!snapshot || Date.now() - loadedAt >= ttlMs) {
|
|
101
|
+
try {
|
|
102
|
+
await load();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (!snapshot)
|
|
106
|
+
throw err;
|
|
107
|
+
// Stale-but-present beats down: keep serving the last good
|
|
108
|
+
// snapshot; a later lookup retries.
|
|
109
|
+
loadedAt = Date.now();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return snapshot?.[role] ?? [];
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
lookup,
|
|
116
|
+
invalidate() {
|
|
117
|
+
snapshot = null;
|
|
118
|
+
loadedAt = 0;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=role-scopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-scopes.js","sourceRoot":"","sources":["../../src/roles/role-scopes.ts"],"names":[],"mappings":"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"}
|
package/dist/server/cookies.d.ts
CHANGED
|
@@ -16,11 +16,15 @@
|
|
|
16
16
|
* so it never leaks to non-auth handlers.
|
|
17
17
|
* - `Path=/` for the id/access tokens, `Path=/auth` for refresh.
|
|
18
18
|
*
|
|
19
|
-
* The cookies are **host-only** (no `Domain` attribute) so
|
|
20
|
-
* scoped to the host the response came from. The browser sends
|
|
21
|
-
* every request to that host, including cross-origin XHR from a
|
|
22
|
-
* subdomain when `credentials: 'include'` is used and both hosts
|
|
23
|
-
* the same registrable domain.
|
|
19
|
+
* The cookies are **host-only** (no `Domain` attribute) by default, so
|
|
20
|
+
* they're scoped to the host the response came from. The browser sends
|
|
21
|
+
* them on every request to that host, including cross-origin XHR from a
|
|
22
|
+
* sibling subdomain when `credentials: 'include'` is used and both hosts
|
|
23
|
+
* share the same registrable domain. Apps that need the session to
|
|
24
|
+
* survive host changes — `www.` ↔ apex on a white-label domain, or
|
|
25
|
+
* cross-subdomain navigation in dev — pass {@link CookieOptions.domain}
|
|
26
|
+
* (usually computed per request with {@link resolveCookieDomain}) and
|
|
27
|
+
* every cookie gains a `Domain=` attribute.
|
|
24
28
|
*/
|
|
25
29
|
export declare const ID_TOKEN_COOKIE = "vk_id_token";
|
|
26
30
|
export declare const ACCESS_TOKEN_COOKIE = "vk_access_token";
|
|
@@ -42,12 +46,32 @@ export interface CookieOptions {
|
|
|
42
46
|
/**
|
|
43
47
|
* Path scope for the refresh cookie. Defaults to `/auth`. The refresh
|
|
44
48
|
* token is never sent on requests outside this prefix.
|
|
49
|
+
*
|
|
50
|
+
* Set this to the auth prefix **as the browser sees it**: an SPA that
|
|
51
|
+
* reaches the API through a same-origin `/api` proxy must use
|
|
52
|
+
* `/api/auth`, otherwise the browser never attaches the refresh
|
|
53
|
+
* cookie to `/api/auth/refresh` and the session dies when the id
|
|
54
|
+
* token expires.
|
|
45
55
|
*/
|
|
46
56
|
refreshPath?: string;
|
|
47
57
|
/**
|
|
48
58
|
* Lifetime of the refresh cookie in seconds. Defaults to 30 days.
|
|
49
59
|
*/
|
|
50
60
|
refreshMaxAgeSeconds?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Emit a `Domain=` attribute on every cookie. Omit (default) for
|
|
63
|
+
* host-only cookies.
|
|
64
|
+
*
|
|
65
|
+
* Needed whenever the session must survive a host change: pinning to
|
|
66
|
+
* the apex so `www.example.com` and `example.com` share the session,
|
|
67
|
+
* or cross-subdomain navigation on a multi-tenant platform. Compute
|
|
68
|
+
* it per request with {@link resolveCookieDomain}.
|
|
69
|
+
*
|
|
70
|
+
* Clears MUST use the same domain as the set: a browser only deletes
|
|
71
|
+
* a cookie when the clear's Domain + Path match the original, so pass
|
|
72
|
+
* the same options to {@link buildClearSessionCookies}.
|
|
73
|
+
*/
|
|
74
|
+
domain?: string;
|
|
51
75
|
}
|
|
52
76
|
/**
|
|
53
77
|
* Default value for the cookie `Secure` flag.
|
|
@@ -105,6 +129,14 @@ export interface OAuthStateCookieOptions {
|
|
|
105
129
|
* sub-second TTLs.
|
|
106
130
|
*/
|
|
107
131
|
maxAgeSeconds?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Emit a `Domain=` attribute. Omit (default) for a host-only cookie —
|
|
134
|
+
* the start and callback requests normally hit the same browser
|
|
135
|
+
* origin, so host-only is almost always right. Accepted so callers
|
|
136
|
+
* threading one {@link CookieOptions} bag through a whole auth flow
|
|
137
|
+
* don't need a special case.
|
|
138
|
+
*/
|
|
139
|
+
domain?: string;
|
|
108
140
|
}
|
|
109
141
|
/**
|
|
110
142
|
* Build the `Set-Cookie` header value pinning the OAuth CSRF `state`
|
|
@@ -136,7 +168,46 @@ export declare function buildOAuthStateCookie(provider: string, value: string, o
|
|
|
136
168
|
* by {@link buildOAuthStateCookie}. Emit at the end of the callback
|
|
137
169
|
* flow so a stale `state` can't be replayed.
|
|
138
170
|
*/
|
|
139
|
-
export declare function clearOAuthStateCookie(provider: string, options?: Pick<OAuthStateCookieOptions, 'secure' | 'path'>): string;
|
|
171
|
+
export declare function clearOAuthStateCookie(provider: string, options?: Pick<OAuthStateCookieOptions, 'secure' | 'path' | 'domain'>): string;
|
|
172
|
+
export interface CookieDomainOptions {
|
|
173
|
+
/**
|
|
174
|
+
* The platform's own registrable domain (e.g. `example.com` for a
|
|
175
|
+
* platform serving tenants on `<slug>.example.com`). Hosts equal to
|
|
176
|
+
* it or ending in `.<platformApex>` are treated as platform hosts;
|
|
177
|
+
* anything else is a white-label custom domain.
|
|
178
|
+
*/
|
|
179
|
+
platformApex: string;
|
|
180
|
+
/**
|
|
181
|
+
* What to do for platform hosts:
|
|
182
|
+
* - `false` (default) — return `undefined`, i.e. host-only cookies.
|
|
183
|
+
* Right for prod multi-tenant platforms where each tenant
|
|
184
|
+
* subdomain is its own session boundary.
|
|
185
|
+
* - `true` — return the platform apex so the session is shared
|
|
186
|
+
* across every platform subdomain. Typical for dev, where the
|
|
187
|
+
* login page and the tenant SPA live on different subdomains of
|
|
188
|
+
* the same wildcard /etc/hosts entry.
|
|
189
|
+
* Custom domains are always pinned to their own apex regardless.
|
|
190
|
+
*/
|
|
191
|
+
pinPlatform?: boolean;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compute the {@link CookieOptions.domain} value for a request host on
|
|
195
|
+
* a white-label multi-tenant platform.
|
|
196
|
+
*
|
|
197
|
+
* Decision table (`platformApex: 'example.com'`):
|
|
198
|
+
* - `app.example.com` → `undefined` (host-only) — or `example.com`
|
|
199
|
+
* with `pinPlatform: true`.
|
|
200
|
+
* - `www.acme.io` / `acme.io` (custom domain) → `acme.io`, so the
|
|
201
|
+
* `www.` and bare hosts share the session.
|
|
202
|
+
* - `''` → `undefined`.
|
|
203
|
+
*
|
|
204
|
+
* Ports are stripped (`app.example.com:3000` → platform host), and
|
|
205
|
+
* matching is case-insensitive. Pass the BROWSER-facing host — behind
|
|
206
|
+
* a same-origin SPA proxy that's the `origin`/`referer` host, not the
|
|
207
|
+
* Lambda's `host` header (see `getRequestHost` in
|
|
208
|
+
* `@venturekit/runtime`).
|
|
209
|
+
*/
|
|
210
|
+
export declare function resolveCookieDomain(requestHost: string, options: CookieDomainOptions): string | undefined;
|
|
140
211
|
/**
|
|
141
212
|
* Read a single cookie out of a `Cookie:` header value. Returns `null`
|
|
142
213
|
* when the cookie is absent. Permissive: duplicates take the last
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAC7C,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AACrD,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AAIvD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,EAAE,CA0BV;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,EAAE,CAY1E;AAUD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,uBAAuB,GAChC,MAAM,CAYR;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC,GACpE,MAAM,CAYR;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,SAAS,CAUpB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAC1C,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAef"}
|
package/dist/server/cookies.js
CHANGED
|
@@ -16,11 +16,15 @@
|
|
|
16
16
|
* so it never leaks to non-auth handlers.
|
|
17
17
|
* - `Path=/` for the id/access tokens, `Path=/auth` for refresh.
|
|
18
18
|
*
|
|
19
|
-
* The cookies are **host-only** (no `Domain` attribute) so
|
|
20
|
-
* scoped to the host the response came from. The browser sends
|
|
21
|
-
* every request to that host, including cross-origin XHR from a
|
|
22
|
-
* subdomain when `credentials: 'include'` is used and both hosts
|
|
23
|
-
* the same registrable domain.
|
|
19
|
+
* The cookies are **host-only** (no `Domain` attribute) by default, so
|
|
20
|
+
* they're scoped to the host the response came from. The browser sends
|
|
21
|
+
* them on every request to that host, including cross-origin XHR from a
|
|
22
|
+
* sibling subdomain when `credentials: 'include'` is used and both hosts
|
|
23
|
+
* share the same registrable domain. Apps that need the session to
|
|
24
|
+
* survive host changes — `www.` ↔ apex on a white-label domain, or
|
|
25
|
+
* cross-subdomain navigation in dev — pass {@link CookieOptions.domain}
|
|
26
|
+
* (usually computed per request with {@link resolveCookieDomain}) and
|
|
27
|
+
* every cookie gains a `Domain=` attribute.
|
|
24
28
|
*/
|
|
25
29
|
export const ID_TOKEN_COOKIE = 'vk_id_token';
|
|
26
30
|
export const ACCESS_TOKEN_COOKIE = 'vk_access_token';
|
|
@@ -46,11 +50,14 @@ function resolve(opts) {
|
|
|
46
50
|
secure: opts?.secure ?? defaultSecure(),
|
|
47
51
|
refreshPath: opts?.refreshPath ?? '/auth',
|
|
48
52
|
refreshMaxAgeSeconds: opts?.refreshMaxAgeSeconds ?? REFRESH_MAX_AGE_SECONDS,
|
|
53
|
+
domain: opts?.domain,
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
function buildSetCookie(name, value, attrs, secure) {
|
|
52
57
|
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
53
58
|
parts.push(`Path=${attrs.path ?? '/'}`);
|
|
59
|
+
if (attrs.domain)
|
|
60
|
+
parts.push(`Domain=${attrs.domain}`);
|
|
54
61
|
parts.push(`SameSite=${attrs.sameSite ?? 'Lax'}`);
|
|
55
62
|
parts.push('HttpOnly');
|
|
56
63
|
if (secure)
|
|
@@ -71,16 +78,17 @@ function buildSetCookie(name, value, attrs, secure) {
|
|
|
71
78
|
* `REFRESH_TOKEN_AUTH` doesn't rotate the refresh token).
|
|
72
79
|
*/
|
|
73
80
|
export function buildSessionCookies(tokens, options) {
|
|
74
|
-
const { secure, refreshPath, refreshMaxAgeSeconds } = resolve(options);
|
|
81
|
+
const { secure, refreshPath, refreshMaxAgeSeconds, domain } = resolve(options);
|
|
75
82
|
const accessMaxAge = Math.max(60, Math.min(tokens.expiresIn, 3600));
|
|
76
83
|
const headers = [];
|
|
77
|
-
headers.push(buildSetCookie(ID_TOKEN_COOKIE, tokens.idToken, { maxAge: accessMaxAge }, secure));
|
|
78
|
-
headers.push(buildSetCookie(ACCESS_TOKEN_COOKIE, tokens.accessToken, { maxAge: accessMaxAge }, secure));
|
|
84
|
+
headers.push(buildSetCookie(ID_TOKEN_COOKIE, tokens.idToken, { maxAge: accessMaxAge, ...(domain ? { domain } : {}) }, secure));
|
|
85
|
+
headers.push(buildSetCookie(ACCESS_TOKEN_COOKIE, tokens.accessToken, { maxAge: accessMaxAge, ...(domain ? { domain } : {}) }, secure));
|
|
79
86
|
if (tokens.refreshToken) {
|
|
80
87
|
headers.push(buildSetCookie(REFRESH_TOKEN_COOKIE, tokens.refreshToken, {
|
|
81
88
|
path: refreshPath,
|
|
82
89
|
maxAge: refreshMaxAgeSeconds,
|
|
83
90
|
sameSite: 'Strict',
|
|
91
|
+
...(domain ? { domain } : {}),
|
|
84
92
|
}, secure));
|
|
85
93
|
}
|
|
86
94
|
return headers;
|
|
@@ -91,11 +99,11 @@ export function buildSessionCookies(tokens, options) {
|
|
|
91
99
|
* terminally so the next attempt goes straight to sign-in.
|
|
92
100
|
*/
|
|
93
101
|
export function buildClearSessionCookies(options) {
|
|
94
|
-
const { secure, refreshPath } = resolve(options);
|
|
102
|
+
const { secure, refreshPath, domain } = resolve(options);
|
|
95
103
|
return [
|
|
96
|
-
buildSetCookie(ID_TOKEN_COOKIE, '', { maxAge: 0 }, secure),
|
|
97
|
-
buildSetCookie(ACCESS_TOKEN_COOKIE, '', { maxAge: 0 }, secure),
|
|
98
|
-
buildSetCookie(REFRESH_TOKEN_COOKIE, '', { path: refreshPath, maxAge: 0, sameSite: 'Strict' }, secure),
|
|
104
|
+
buildSetCookie(ID_TOKEN_COOKIE, '', { maxAge: 0, ...(domain ? { domain } : {}) }, secure),
|
|
105
|
+
buildSetCookie(ACCESS_TOKEN_COOKIE, '', { maxAge: 0, ...(domain ? { domain } : {}) }, secure),
|
|
106
|
+
buildSetCookie(REFRESH_TOKEN_COOKIE, '', { path: refreshPath, maxAge: 0, sameSite: 'Strict', ...(domain ? { domain } : {}) }, secure),
|
|
99
107
|
];
|
|
100
108
|
}
|
|
101
109
|
/**
|
|
@@ -144,6 +152,7 @@ export function buildOAuthStateCookie(provider, value, options) {
|
|
|
144
152
|
return buildSetCookie(oauthStateCookieName(provider), value, {
|
|
145
153
|
path: options?.path ?? '/auth',
|
|
146
154
|
maxAge: options?.maxAgeSeconds ?? OAUTH_STATE_MAX_AGE_SECONDS,
|
|
155
|
+
...(options?.domain ? { domain: options.domain } : {}),
|
|
147
156
|
}, secure);
|
|
148
157
|
}
|
|
149
158
|
/**
|
|
@@ -153,7 +162,40 @@ export function buildOAuthStateCookie(provider, value, options) {
|
|
|
153
162
|
*/
|
|
154
163
|
export function clearOAuthStateCookie(provider, options) {
|
|
155
164
|
const secure = options?.secure ?? defaultSecure();
|
|
156
|
-
return buildSetCookie(oauthStateCookieName(provider), '', {
|
|
165
|
+
return buildSetCookie(oauthStateCookieName(provider), '', {
|
|
166
|
+
path: options?.path ?? '/auth',
|
|
167
|
+
maxAge: 0,
|
|
168
|
+
...(options?.domain ? { domain: options.domain } : {}),
|
|
169
|
+
}, secure);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compute the {@link CookieOptions.domain} value for a request host on
|
|
173
|
+
* a white-label multi-tenant platform.
|
|
174
|
+
*
|
|
175
|
+
* Decision table (`platformApex: 'example.com'`):
|
|
176
|
+
* - `app.example.com` → `undefined` (host-only) — or `example.com`
|
|
177
|
+
* with `pinPlatform: true`.
|
|
178
|
+
* - `www.acme.io` / `acme.io` (custom domain) → `acme.io`, so the
|
|
179
|
+
* `www.` and bare hosts share the session.
|
|
180
|
+
* - `''` → `undefined`.
|
|
181
|
+
*
|
|
182
|
+
* Ports are stripped (`app.example.com:3000` → platform host), and
|
|
183
|
+
* matching is case-insensitive. Pass the BROWSER-facing host — behind
|
|
184
|
+
* a same-origin SPA proxy that's the `origin`/`referer` host, not the
|
|
185
|
+
* Lambda's `host` header (see `getRequestHost` in
|
|
186
|
+
* `@venturekit/runtime`).
|
|
187
|
+
*/
|
|
188
|
+
export function resolveCookieDomain(requestHost, options) {
|
|
189
|
+
const host = requestHost.replace(/:\d+$/, '').toLowerCase();
|
|
190
|
+
if (!host)
|
|
191
|
+
return undefined;
|
|
192
|
+
const apex = options.platformApex.toLowerCase();
|
|
193
|
+
if (host === apex || host.endsWith(`.${apex}`)) {
|
|
194
|
+
return options.pinPlatform ? apex : undefined;
|
|
195
|
+
}
|
|
196
|
+
// Custom (white-label) domain — pin to the apex so `www.` and the
|
|
197
|
+
// bare domain share the session.
|
|
198
|
+
return host.replace(/^www\./, '');
|
|
157
199
|
}
|
|
158
200
|
/**
|
|
159
201
|
* Read a single cookie out of a `Cookie:` header value. Returns `null`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"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"}
|