@assemora/auth 0.1.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/LICENSE +202 -0
- package/README.md +69 -0
- package/dist/actors.d.ts +20 -0
- package/dist/actors.d.ts.map +1 -0
- package/dist/actors.js +37 -0
- package/dist/actors.js.map +1 -0
- package/dist/authorization.d.ts +36 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +93 -0
- package/dist/authorization.js.map +1 -0
- package/dist/commands.d.ts +232 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +390 -0
- package/dist/commands.js.map +1 -0
- package/dist/credentials.d.ts +16 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +43 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +136 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +92 -0
- package/dist/models.js.map +1 -0
- package/dist/module.d.ts +39 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +95 -0
- package/dist/module.js.map +1 -0
- package/dist/ownership.d.ts +67 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/ownership.js +51 -0
- package/dist/ownership.js.map +1 -0
- package/dist/permissions.d.ts +40 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +86 -0
- package/dist/permissions.js.map +1 -0
- package/dist/policies.d.ts +120 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +92 -0
- package/dist/policies.js.map +1 -0
- package/dist/queries.d.ts +166 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +236 -0
- package/dist/queries.js.map +1 -0
- package/dist/sessions.d.ts +40 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +39 -0
- package/dist/sessions.js.map +1 -0
- package/dist/tokens.d.ts +40 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +60 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const hashPassword: (password: string) => Promise<string>;
|
|
2
|
+
export declare const verifyPassword: (stored: string, password: string) => Promise<boolean>;
|
|
3
|
+
export type IssuedToken = {
|
|
4
|
+
/** Shown once, at creation. Nothing stores it. */
|
|
5
|
+
readonly token: string;
|
|
6
|
+
/** What the database keeps. */
|
|
7
|
+
readonly hash: string;
|
|
8
|
+
};
|
|
9
|
+
/** 256 bits of randomness, url-safe, with a prefix that says what it opens. */
|
|
10
|
+
export declare const issueToken: (prefix: string) => IssuedToken;
|
|
11
|
+
export declare const hashToken: (token: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Compares two digests without leaking, through timing, how much of one matched.
|
|
14
|
+
*/
|
|
15
|
+
export declare const tokensMatch: (left: string, right: string) => boolean;
|
|
16
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,YAAY,aAAc,MAAM,KAAG,OAAO,CAAC,MAAM,CAAiC,CAAA;AAE/F,eAAO,MAAM,cAAc,WAAkB,MAAM,YAAY,MAAM,KAAG,OAAO,CAAC,OAAO,CAQtF,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,UAAU,WAAY,MAAM,KAAG,WAI3C,CAAA;AAED,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,MAA0D,CAAA;AAEpG;;GAEG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,SAAS,MAAM,KAAG,OAKzD,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password and token secrets (SPEC.md §49, §85).
|
|
3
|
+
*
|
|
4
|
+
* The two are hashed differently on purpose. A password is chosen by a person, so it
|
|
5
|
+
* is low in entropy and needs a slow, memory-hard hash — Argon2id. A token is 256
|
|
6
|
+
* bits from a CSPRNG, so guessing it is already impossible; hashing it slowly would
|
|
7
|
+
* cost every request and buy nothing, and SHA-256 is the right tool. Both are stored
|
|
8
|
+
* as digests and never as written.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
11
|
+
import { hash as argon2Hash, verify as argon2Verify } from '@node-rs/argon2';
|
|
12
|
+
/** OWASP's recommended Argon2id parameters, kept in one place to be raised together. */
|
|
13
|
+
const ARGON2 = {
|
|
14
|
+
memoryCost: 19_456,
|
|
15
|
+
timeCost: 2,
|
|
16
|
+
parallelism: 1,
|
|
17
|
+
};
|
|
18
|
+
export const hashPassword = (password) => argon2Hash(password, ARGON2);
|
|
19
|
+
export const verifyPassword = async (stored, password) => {
|
|
20
|
+
try {
|
|
21
|
+
return await argon2Verify(stored, password);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// A malformed or truncated hash is a failed verification, never an exception
|
|
25
|
+
// that a caller might treat as a success.
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** 256 bits of randomness, url-safe, with a prefix that says what it opens. */
|
|
30
|
+
export const issueToken = (prefix) => {
|
|
31
|
+
const token = `${prefix}_${randomBytes(32).toString('base64url')}`;
|
|
32
|
+
return { token, hash: hashToken(token) };
|
|
33
|
+
};
|
|
34
|
+
export const hashToken = (token) => createHash('sha256').update(token).digest('hex');
|
|
35
|
+
/**
|
|
36
|
+
* Compares two digests without leaking, through timing, how much of one matched.
|
|
37
|
+
*/
|
|
38
|
+
export const tokensMatch = (left, right) => {
|
|
39
|
+
const a = Buffer.from(left, 'utf8');
|
|
40
|
+
const b = Buffer.from(right, 'utf8');
|
|
41
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEtE,OAAO,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE5E,wFAAwF;AACxF,MAAM,MAAM,GAAG;IACb,UAAU,EAAE,MAAM;IAClB,QAAQ,EAAE,CAAC;IACX,WAAW,EAAE,CAAC;CACN,CAAA;AAEV,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAmB,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAE/F,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,MAAc,EAAE,QAAgB,EAAoB,EAAE;IACzF,IAAI,CAAC;QACH,OAAO,MAAM,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;QAC7E,0CAA0C;QAC1C,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AASD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAc,EAAe,EAAE;IACxD,MAAM,KAAK,GAAG,GAAG,MAAM,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAA;IAElE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAA;AAC1C,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAEpG;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,KAAa,EAAW,EAAE;IAClE,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAEpC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AACvD,CAAC,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@assemora/auth` — who is asking, and what they may do.
|
|
3
|
+
*
|
|
4
|
+
* Core has declared the authorization port since phase 1 and denied everything in
|
|
5
|
+
* its absence. This package is the implementation: roles and permissions from
|
|
6
|
+
* SPEC.md §50, policies from §51, sessions and tokens from §49.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* export const ArticlePolicy = policy('articles', {
|
|
10
|
+
* read: () => true,
|
|
11
|
+
* update: ({ actor, record }) => actor?.id === record.authorId,
|
|
12
|
+
* delete: ({ can }) => can('articles.delete'),
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* const app = createApplication({
|
|
16
|
+
* modules: [auth({ policies: [ArticlePolicy] }), blog()],
|
|
17
|
+
* authorization: policies(),
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Nothing sensitive is stored as written: passwords are Argon2id, tokens are
|
|
22
|
+
* digests, and a token's plaintext exists exactly once — when it is issued.
|
|
23
|
+
*/
|
|
24
|
+
export { resolveActor, SESSION_COOKIE } from './actors.js';
|
|
25
|
+
export { type CommandSubject, policies, subjectOf } from './authorization.js';
|
|
26
|
+
export { authCommands, CreateAgent, CreateApiToken, CreateRole, CreateUser, DeleteRole, GrantRole, publicAuthPolicy, RevokeApiToken, RevokeRole, SetPassword, SignIn, SignOut, UpdateAgent, UpdateRole, UpdateUser, } from './commands.js';
|
|
27
|
+
export { hashPassword, hashToken, type IssuedToken, issueToken, tokensMatch, verifyPassword, } from './credentials.js';
|
|
28
|
+
export { Agent, AgentToken, ApiToken, authModels, Permission, Role, RolePermission, Session, User, UserRole, } from './models.js';
|
|
29
|
+
export { type AuthModuleOptions, auth, definePolicyFacet } from './module.js';
|
|
30
|
+
export { holds, type PermissionSet, permissionsOf, WILDCARD, } from './permissions.js';
|
|
31
|
+
export { clearPolicies, describedPolicies, describePolicy, type Policy, type PolicyActor, type PolicyContext, type PolicyDescriptor, type PolicyRule, type PolicyRules, policy, policyFor, registeredPolicies, registerPolicy, } from './policies.js';
|
|
32
|
+
export { authQueries, GetUser, ListAgents, ListApiTokens, ListPermissions, ListRoles, ListUsers, } from './queries.js';
|
|
33
|
+
export { DEFAULT_SESSION_TTL_MS, endSession, purgeExpiredSessions, SESSION_PREFIX, type SessionDetails, type StartedSession, sessionActor, startSession, } from './sessions.js';
|
|
34
|
+
export { AGENT_TOKEN_PREFIX, type AgentDetails, API_TOKEN_PREFIX, type ApiTokenDetails, type CreatedAgent, createAgent, createApiToken, type IssuedApiToken, revokeAgentToken, revokeApiToken, tokenActor, } from './tokens.js';
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,EACV,UAAU,EACV,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,WAAW,EACX,MAAM,EACN,OAAO,EACP,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,KAAK,WAAW,EAChB,UAAU,EACV,WAAW,EACX,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,EACL,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,IAAI,EACJ,cAAc,EACd,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,KAAK,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EACL,KAAK,EACL,KAAK,aAAa,EAClB,aAAa,EACb,QAAQ,GACT,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,MAAM,EACN,SAAS,EACT,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,WAAW,EACX,OAAO,EACP,UAAU,EACV,aAAa,EACb,eAAe,EACf,SAAS,EACT,SAAS,GACV,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,sBAAsB,EACtB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,YAAY,EACZ,YAAY,GACb,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EACjB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,WAAW,EACX,cAAc,EACd,KAAK,cAAc,EACnB,gBAAgB,EAChB,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@assemora/auth` — who is asking, and what they may do.
|
|
3
|
+
*
|
|
4
|
+
* Core has declared the authorization port since phase 1 and denied everything in
|
|
5
|
+
* its absence. This package is the implementation: roles and permissions from
|
|
6
|
+
* SPEC.md §50, policies from §51, sessions and tokens from §49.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* export const ArticlePolicy = policy('articles', {
|
|
10
|
+
* read: () => true,
|
|
11
|
+
* update: ({ actor, record }) => actor?.id === record.authorId,
|
|
12
|
+
* delete: ({ can }) => can('articles.delete'),
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* const app = createApplication({
|
|
16
|
+
* modules: [auth({ policies: [ArticlePolicy] }), blog()],
|
|
17
|
+
* authorization: policies(),
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Nothing sensitive is stored as written: passwords are Argon2id, tokens are
|
|
22
|
+
* digests, and a token's plaintext exists exactly once — when it is issued.
|
|
23
|
+
*/
|
|
24
|
+
export { resolveActor, SESSION_COOKIE } from './actors.js';
|
|
25
|
+
export { policies, subjectOf } from './authorization.js';
|
|
26
|
+
export { authCommands, CreateAgent, CreateApiToken, CreateRole, CreateUser, DeleteRole, GrantRole, publicAuthPolicy, RevokeApiToken, RevokeRole, SetPassword, SignIn, SignOut, UpdateAgent, UpdateRole, UpdateUser, } from './commands.js';
|
|
27
|
+
export { hashPassword, hashToken, issueToken, tokensMatch, verifyPassword, } from './credentials.js';
|
|
28
|
+
export { Agent, AgentToken, ApiToken, authModels, Permission, Role, RolePermission, Session, User, UserRole, } from './models.js';
|
|
29
|
+
export { auth, definePolicyFacet } from './module.js';
|
|
30
|
+
export { holds, permissionsOf, WILDCARD, } from './permissions.js';
|
|
31
|
+
export { clearPolicies, describedPolicies, describePolicy, policy, policyFor, registeredPolicies, registerPolicy, } from './policies.js';
|
|
32
|
+
export { authQueries, GetUser, ListAgents, ListApiTokens, ListPermissions, ListRoles, ListUsers, } from './queries.js';
|
|
33
|
+
export { DEFAULT_SESSION_TTL_MS, endSession, purgeExpiredSessions, SESSION_PREFIX, sessionActor, startSession, } from './sessions.js';
|
|
34
|
+
export { AGENT_TOKEN_PREFIX, API_TOKEN_PREFIX, createAgent, createApiToken, revokeAgentToken, revokeApiToken, tokenActor, } from './tokens.js';
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EAAuB,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,EACV,UAAU,EACV,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,UAAU,EACV,WAAW,EACX,MAAM,EACN,OAAO,EACP,WAAW,EACX,UAAU,EACV,UAAU,GACX,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,YAAY,EACZ,SAAS,EAET,UAAU,EACV,WAAW,EACX,cAAc,GACf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,KAAK,EACL,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,IAAI,EACJ,cAAc,EACd,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,MAAM,aAAa,CAAA;AACpB,OAAO,EAA0B,IAAI,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EACL,KAAK,EAEL,aAAa,EACb,QAAQ,GACT,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EAOd,MAAM,EACN,SAAS,EACT,kBAAkB,EAClB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,WAAW,EACX,OAAO,EACP,UAAU,EACV,aAAa,EACb,eAAe,EACf,SAAS,EACT,SAAS,GACV,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,sBAAsB,EACtB,UAAU,EACV,oBAAoB,EACpB,cAAc,EAGd,YAAY,EACZ,YAAY,GACb,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kBAAkB,EAElB,gBAAgB,EAGhB,WAAW,EACX,cAAc,EAEd,gBAAgB,EAChB,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAA"}
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export declare const User: import("@assemora/data").Model<{
|
|
2
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
3
|
+
email: import("@assemora/data").ColumnBuilder<string>;
|
|
4
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
5
|
+
/** Argon2id. Never leaves the database and never reaches serialized output. */
|
|
6
|
+
passwordHash: import("@assemora/data").ColumnBuilder<string>;
|
|
7
|
+
active: import("@assemora/data").ColumnBuilder<boolean>;
|
|
8
|
+
/** Studio states the version it read, and a stale edit is a 409 (SPEC.md §66). */
|
|
9
|
+
version: import("@assemora/data").ColumnBuilder<number>;
|
|
10
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
11
|
+
updatedAt: import("@assemora/data").TimestampColumnBuilder;
|
|
12
|
+
}, never, Readonly<Record<never, never>>>;
|
|
13
|
+
export declare const Session: import("@assemora/data").Model<{
|
|
14
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
15
|
+
/** A digest of the session secret; the secret itself lives only in the cookie. */
|
|
16
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
17
|
+
userId: import("@assemora/data").UuidColumnBuilder;
|
|
18
|
+
userAgent: import("@assemora/data").ColumnBuilder<string | null>;
|
|
19
|
+
ipAddress: import("@assemora/data").ColumnBuilder<string | null>;
|
|
20
|
+
expiresAt: import("@assemora/data").TimestampColumnBuilder;
|
|
21
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
22
|
+
}, never, Readonly<Record<never, never>>>;
|
|
23
|
+
export declare const Role: import("@assemora/data").Model<{
|
|
24
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
25
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
26
|
+
label: import("@assemora/data").ColumnBuilder<string>;
|
|
27
|
+
version: import("@assemora/data").ColumnBuilder<number>;
|
|
28
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
29
|
+
}, never, Readonly<Record<never, never>>>;
|
|
30
|
+
export declare const Permission: import("@assemora/data").Model<{
|
|
31
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
32
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
33
|
+
description: import("@assemora/data").ColumnBuilder<string | null>;
|
|
34
|
+
}, never, Readonly<Record<never, never>>>;
|
|
35
|
+
export declare const UserRole: import("@assemora/data").Model<{
|
|
36
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
37
|
+
userId: import("@assemora/data").UuidColumnBuilder;
|
|
38
|
+
roleId: import("@assemora/data").UuidColumnBuilder;
|
|
39
|
+
}, never, Readonly<Record<never, never>>>;
|
|
40
|
+
export declare const RolePermission: import("@assemora/data").Model<{
|
|
41
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
42
|
+
roleId: import("@assemora/data").UuidColumnBuilder;
|
|
43
|
+
permissionId: import("@assemora/data").UuidColumnBuilder;
|
|
44
|
+
}, never, Readonly<Record<never, never>>>;
|
|
45
|
+
export declare const ApiToken: import("@assemora/data").Model<{
|
|
46
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
47
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
48
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
49
|
+
userId: import("@assemora/data").ColumnBuilder<string | null>;
|
|
50
|
+
permissions: import("@assemora/data").JsonColumnBuilder<readonly string[]>;
|
|
51
|
+
expiresAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
52
|
+
lastUsedAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
53
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
54
|
+
}, never, Readonly<Record<never, never>>>;
|
|
55
|
+
export declare const Agent: import("@assemora/data").Model<{
|
|
56
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
57
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
58
|
+
description: import("@assemora/data").ColumnBuilder<string | null>;
|
|
59
|
+
permissions: import("@assemora/data").JsonColumnBuilder<readonly string[]>;
|
|
60
|
+
enabled: import("@assemora/data").ColumnBuilder<boolean>;
|
|
61
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
62
|
+
}, never, Readonly<Record<never, never>>>;
|
|
63
|
+
export declare const AgentToken: import("@assemora/data").Model<{
|
|
64
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
65
|
+
agentId: import("@assemora/data").UuidColumnBuilder;
|
|
66
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
67
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
68
|
+
expiresAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
69
|
+
lastUsedAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
70
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
71
|
+
}, never, Readonly<Record<never, never>>>;
|
|
72
|
+
/** Every table this package owns, for schema generation and for tests. */
|
|
73
|
+
export declare const authModels: readonly [import("@assemora/data").Model<{
|
|
74
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
75
|
+
email: import("@assemora/data").ColumnBuilder<string>;
|
|
76
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
77
|
+
/** Argon2id. Never leaves the database and never reaches serialized output. */
|
|
78
|
+
passwordHash: import("@assemora/data").ColumnBuilder<string>;
|
|
79
|
+
active: import("@assemora/data").ColumnBuilder<boolean>;
|
|
80
|
+
/** Studio states the version it read, and a stale edit is a 409 (SPEC.md §66). */
|
|
81
|
+
version: import("@assemora/data").ColumnBuilder<number>;
|
|
82
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
83
|
+
updatedAt: import("@assemora/data").TimestampColumnBuilder;
|
|
84
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
85
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
86
|
+
/** A digest of the session secret; the secret itself lives only in the cookie. */
|
|
87
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
88
|
+
userId: import("@assemora/data").UuidColumnBuilder;
|
|
89
|
+
userAgent: import("@assemora/data").ColumnBuilder<string | null>;
|
|
90
|
+
ipAddress: import("@assemora/data").ColumnBuilder<string | null>;
|
|
91
|
+
expiresAt: import("@assemora/data").TimestampColumnBuilder;
|
|
92
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
93
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
94
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
95
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
96
|
+
label: import("@assemora/data").ColumnBuilder<string>;
|
|
97
|
+
version: import("@assemora/data").ColumnBuilder<number>;
|
|
98
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
99
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
100
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
101
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
102
|
+
description: import("@assemora/data").ColumnBuilder<string | null>;
|
|
103
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
104
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
105
|
+
userId: import("@assemora/data").UuidColumnBuilder;
|
|
106
|
+
roleId: import("@assemora/data").UuidColumnBuilder;
|
|
107
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
108
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
109
|
+
roleId: import("@assemora/data").UuidColumnBuilder;
|
|
110
|
+
permissionId: import("@assemora/data").UuidColumnBuilder;
|
|
111
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
112
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
113
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
114
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
115
|
+
userId: import("@assemora/data").ColumnBuilder<string | null>;
|
|
116
|
+
permissions: import("@assemora/data").JsonColumnBuilder<readonly string[]>;
|
|
117
|
+
expiresAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
118
|
+
lastUsedAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
119
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
120
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
121
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
122
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
123
|
+
description: import("@assemora/data").ColumnBuilder<string | null>;
|
|
124
|
+
permissions: import("@assemora/data").JsonColumnBuilder<readonly string[]>;
|
|
125
|
+
enabled: import("@assemora/data").ColumnBuilder<boolean>;
|
|
126
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
127
|
+
}, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
|
|
128
|
+
id: import("@assemora/data").UuidColumnBuilder;
|
|
129
|
+
agentId: import("@assemora/data").UuidColumnBuilder;
|
|
130
|
+
name: import("@assemora/data").ColumnBuilder<string>;
|
|
131
|
+
tokenHash: import("@assemora/data").ColumnBuilder<string>;
|
|
132
|
+
expiresAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
133
|
+
lastUsedAt: import("@assemora/data").ColumnBuilder<Date | null>;
|
|
134
|
+
createdAt: import("@assemora/data").TimestampColumnBuilder;
|
|
135
|
+
}, never, Readonly<Record<never, never>>>];
|
|
136
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,IAAI;;;;IAIf,+EAA+E;;;IAG/E,kFAAkF;;;;yCAIlF,CAAA;AAEF,eAAO,MAAM,OAAO;;IAElB,kFAAkF;;;;;;;yCAOlF,CAAA;AAEF,eAAO,MAAM,IAAI;;;;;;yCAMf,CAAA;AAEF,eAAO,MAAM,UAAU;;;;yCAIrB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;yCAInB,CAAA;AAEF,eAAO,MAAM,cAAc;;;;yCAIzB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;yCASnB,CAAA;AAEF,eAAO,MAAM,KAAK;;;;;;;yCAOhB,CAAA;AAEF,eAAO,MAAM,UAAU;;;;;;;;yCAQrB,CAAA;AAEF,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;IA7ErB,+EAA+E;;;IAG/E,kFAAkF;;;;;;IAQlF,kFAAkF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0CA4E1E,CAAA"}
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tables authentication and authorization live in (SPEC.md §50).
|
|
3
|
+
*
|
|
4
|
+
* Nothing sensitive is stored as written: a password is an Argon2id hash and a token
|
|
5
|
+
* is a digest. What a caller sees once, at creation, is the only time the plaintext
|
|
6
|
+
* exists (SPEC.md §49, §85).
|
|
7
|
+
*/
|
|
8
|
+
import { boolean, integer, json, model, string, text, timestamp, uuid } from '@assemora/data';
|
|
9
|
+
export const User = model('assemora_users', {
|
|
10
|
+
id: uuid().primary().defaultRandom(),
|
|
11
|
+
email: string().unique(),
|
|
12
|
+
name: string(),
|
|
13
|
+
/** Argon2id. Never leaves the database and never reaches serialized output. */
|
|
14
|
+
passwordHash: string().hidden(),
|
|
15
|
+
active: boolean().default(true),
|
|
16
|
+
/** Studio states the version it read, and a stale edit is a 409 (SPEC.md §66). */
|
|
17
|
+
version: integer().default(1),
|
|
18
|
+
createdAt: timestamp().created(),
|
|
19
|
+
updatedAt: timestamp().updated(),
|
|
20
|
+
});
|
|
21
|
+
export const Session = model('assemora_sessions', {
|
|
22
|
+
id: uuid().primary().defaultRandom(),
|
|
23
|
+
/** A digest of the session secret; the secret itself lives only in the cookie. */
|
|
24
|
+
tokenHash: string().unique().hidden(),
|
|
25
|
+
userId: uuid(),
|
|
26
|
+
userAgent: string().nullable(),
|
|
27
|
+
ipAddress: string().nullable(),
|
|
28
|
+
expiresAt: timestamp(),
|
|
29
|
+
createdAt: timestamp().created(),
|
|
30
|
+
});
|
|
31
|
+
export const Role = model('assemora_roles', {
|
|
32
|
+
id: uuid().primary().defaultRandom(),
|
|
33
|
+
name: string().unique(),
|
|
34
|
+
label: string(),
|
|
35
|
+
version: integer().default(1),
|
|
36
|
+
createdAt: timestamp().created(),
|
|
37
|
+
});
|
|
38
|
+
export const Permission = model('assemora_permissions', {
|
|
39
|
+
id: uuid().primary().defaultRandom(),
|
|
40
|
+
name: string().unique(),
|
|
41
|
+
description: text().nullable(),
|
|
42
|
+
});
|
|
43
|
+
export const UserRole = model('assemora_user_roles', {
|
|
44
|
+
id: uuid().primary().defaultRandom(),
|
|
45
|
+
userId: uuid(),
|
|
46
|
+
roleId: uuid(),
|
|
47
|
+
});
|
|
48
|
+
export const RolePermission = model('assemora_role_permissions', {
|
|
49
|
+
id: uuid().primary().defaultRandom(),
|
|
50
|
+
roleId: uuid(),
|
|
51
|
+
permissionId: uuid(),
|
|
52
|
+
});
|
|
53
|
+
export const ApiToken = model('assemora_api_tokens', {
|
|
54
|
+
id: uuid().primary().defaultRandom(),
|
|
55
|
+
name: string(),
|
|
56
|
+
tokenHash: string().unique().hidden(),
|
|
57
|
+
userId: uuid().nullable(),
|
|
58
|
+
permissions: json(),
|
|
59
|
+
expiresAt: timestamp().nullable(),
|
|
60
|
+
lastUsedAt: timestamp().nullable(),
|
|
61
|
+
createdAt: timestamp().created(),
|
|
62
|
+
});
|
|
63
|
+
export const Agent = model('assemora_agents', {
|
|
64
|
+
id: uuid().primary().defaultRandom(),
|
|
65
|
+
name: string().unique(),
|
|
66
|
+
description: text().nullable(),
|
|
67
|
+
permissions: json(),
|
|
68
|
+
enabled: boolean().default(true),
|
|
69
|
+
createdAt: timestamp().created(),
|
|
70
|
+
});
|
|
71
|
+
export const AgentToken = model('assemora_agent_tokens', {
|
|
72
|
+
id: uuid().primary().defaultRandom(),
|
|
73
|
+
agentId: uuid(),
|
|
74
|
+
name: string(),
|
|
75
|
+
tokenHash: string().unique().hidden(),
|
|
76
|
+
expiresAt: timestamp().nullable(),
|
|
77
|
+
lastUsedAt: timestamp().nullable(),
|
|
78
|
+
createdAt: timestamp().created(),
|
|
79
|
+
});
|
|
80
|
+
/** Every table this package owns, for schema generation and for tests. */
|
|
81
|
+
export const authModels = [
|
|
82
|
+
User,
|
|
83
|
+
Session,
|
|
84
|
+
Role,
|
|
85
|
+
Permission,
|
|
86
|
+
UserRole,
|
|
87
|
+
RolePermission,
|
|
88
|
+
ApiToken,
|
|
89
|
+
Agent,
|
|
90
|
+
AgentToken,
|
|
91
|
+
];
|
|
92
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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"}
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `auth()` module (SPEC.md §9, §113).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* export default assemora({ modules: [auth(), blog()] })
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* Registering it is what replaces `permitAll()`: the authorization port core has
|
|
9
|
+
* declared since phase 1 finally has an implementation, and every command and query
|
|
10
|
+
* starts passing roles, permissions and policies (ADR-0008).
|
|
11
|
+
*/
|
|
12
|
+
import { type ModuleBuilder } from '@assemora/core';
|
|
13
|
+
import { type Policy } from './policies.js';
|
|
14
|
+
declare module '@assemora/core' {
|
|
15
|
+
interface ModuleBuilder {
|
|
16
|
+
/** Registers policies for this module's subjects (SPEC.md §51). */
|
|
17
|
+
policies(...policies: Policy<never>[]): ModuleBuilder;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export declare const definePolicyFacet: () => void;
|
|
21
|
+
export type AuthModuleOptions = {
|
|
22
|
+
/**
|
|
23
|
+
* The application's own policies (SPEC.md §51, ADR-0027).
|
|
24
|
+
*
|
|
25
|
+
* A module may only write a policy for a subject it declares, because a policy is a
|
|
26
|
+
* grant and an installed package must not be able to open the application with one.
|
|
27
|
+
* This is the other door, and it is the application's: it is written at the
|
|
28
|
+
* composition root, by whoever assembled the modules, so it speaks for the whole
|
|
29
|
+
* application and is held to no ownership rule.
|
|
30
|
+
*
|
|
31
|
+
* A policy over a module's own subject belongs on that module —
|
|
32
|
+
* `module('blog').resources(Articles).policies(ArticlePolicy)`. Use this for a policy
|
|
33
|
+
* over somebody else's, which is a decision the application is entitled to make and
|
|
34
|
+
* a package is not.
|
|
35
|
+
*/
|
|
36
|
+
readonly policies?: readonly Policy<never>[];
|
|
37
|
+
};
|
|
38
|
+
export declare const auth: (options?: AuthModuleOptions) => ModuleBuilder;
|
|
39
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAyC,KAAK,aAAa,EAAU,MAAM,gBAAgB,CAAA;AAKlG,OAAO,EAIL,KAAK,MAAM,EAGZ,MAAM,eAAe,CAAA;AAGtB,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,aAAa;QACrB,mEAAmE;QACnE,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,aAAa,CAAA;KACtD;CACF;AAkBD,eAAO,MAAM,iBAAiB,QAAO,IA4BpC,CAAA;AAID,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,IAAI,aAAa,iBAAiB,KAAQ,aA6CtD,CAAA"}
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `auth()` module (SPEC.md §9, §113).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* export default assemora({ modules: [auth(), blog()] })
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* Registering it is what replaces `permitAll()`: the authorization port core has
|
|
9
|
+
* declared since phase 1 finally has an implementation, and every command and query
|
|
10
|
+
* starts passing roles, permissions and policies (ADR-0008).
|
|
11
|
+
*/
|
|
12
|
+
import { ConfigurationError, defineModuleFacet, module } from '@assemora/core';
|
|
13
|
+
import { authCommands, publicAuthPolicy } from './commands.js';
|
|
14
|
+
import { authModels } from './models.js';
|
|
15
|
+
import { foreignSubject, ownsSubject, unattributedSubject } from './ownership.js';
|
|
16
|
+
import { APPLICATION, describedPolicies, describePolicy, policySources, registerModulePolicy, } from './policies.js';
|
|
17
|
+
import { authQueries } from './queries.js';
|
|
18
|
+
let defined = false;
|
|
19
|
+
/**
|
|
20
|
+
* The policies the application passed to `auth({ policies })`, by identity.
|
|
21
|
+
*
|
|
22
|
+
* They travel through the same facet as a module's own — so they are registered at the
|
|
23
|
+
* same moment, and an application that is created and never booted has them, which is
|
|
24
|
+
* what a module's `.policies()` has always given. What differs is only the attribution,
|
|
25
|
+
* and it cannot be a second builder method: a facet is a method on *every* module, and
|
|
26
|
+
* "register a policy for a subject you do not own" is precisely what no module may have.
|
|
27
|
+
*
|
|
28
|
+
* Weak, and private to this file. A package cannot reach it, which is what makes the
|
|
29
|
+
* exemption the application's rather than anybody's.
|
|
30
|
+
*/
|
|
31
|
+
const applicationOwned = new WeakSet();
|
|
32
|
+
export const definePolicyFacet = () => {
|
|
33
|
+
if (defined)
|
|
34
|
+
return;
|
|
35
|
+
defineModuleFacet('policies', (internals, args) => {
|
|
36
|
+
internals.addRegistration((context) => {
|
|
37
|
+
for (const candidate of args) {
|
|
38
|
+
const declared = candidate;
|
|
39
|
+
if (declared?.node !== 'policy')
|
|
40
|
+
continue;
|
|
41
|
+
const application = applicationOwned.has(declared);
|
|
42
|
+
registerModulePolicy(declared, application ? APPLICATION : context.module);
|
|
43
|
+
// Described where it is registered, by the module that registered it. A policy
|
|
44
|
+
// grants access, so the one thing the single source must not be silent about
|
|
45
|
+
// is which package put one there (ADR-0002, ADR-0027). The application's carry
|
|
46
|
+
// no module, which is what "not a package" has looked like since the section
|
|
47
|
+
// existed.
|
|
48
|
+
context.registry.register('policies', application ? describePolicy(declared) : describePolicy(declared, context.module));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
defined = true;
|
|
53
|
+
};
|
|
54
|
+
definePolicyFacet();
|
|
55
|
+
export const auth = (options = {}) => {
|
|
56
|
+
// Marked before the module is built, so the facet knows which of the policies it is
|
|
57
|
+
// handed speak for the application rather than for `auth`.
|
|
58
|
+
for (const declared of options.policies ?? [])
|
|
59
|
+
applicationOwned.add(declared);
|
|
60
|
+
return (module('auth')
|
|
61
|
+
.models(...authModels)
|
|
62
|
+
.commands(...authCommands)
|
|
63
|
+
.queries(...authQueries)
|
|
64
|
+
.policies(publicAuthPolicy, ...(options.policies ?? []))
|
|
65
|
+
/**
|
|
66
|
+
* Every policy, held to the rule that a module speaks only for what it declared.
|
|
67
|
+
*
|
|
68
|
+
* At boot rather than at registration, and that is not a detail: ownership is
|
|
69
|
+
* decided against what the module *registered*, and a facet runs in the order the
|
|
70
|
+
* builder was written. `.policies(P).resources(Articles)` would be refused and
|
|
71
|
+
* `.resources(Articles).policies(P)` allowed, for one declaration. By boot every
|
|
72
|
+
* module has registered everything, so the answer no longer depends on the order
|
|
73
|
+
* somebody typed two lines in.
|
|
74
|
+
*
|
|
75
|
+
* The sweep for what got in through no module at all stays, and is now a refusal
|
|
76
|
+
* rather than a note: an unattributed policy is exactly the registration nothing
|
|
77
|
+
* else records, and describing it only ever made it visible.
|
|
78
|
+
*/
|
|
79
|
+
.boot((context) => {
|
|
80
|
+
const described = new Set(context.registry.section('policies').map((entry) => entry.name));
|
|
81
|
+
const sources = policySources();
|
|
82
|
+
for (const descriptor of describedPolicies()) {
|
|
83
|
+
const source = sources.get(descriptor.name);
|
|
84
|
+
if (source === undefined)
|
|
85
|
+
throw new ConfigurationError(unattributedSubject(descriptor.name));
|
|
86
|
+
if (source !== APPLICATION && !ownsSubject(context.registry, source, descriptor.name)) {
|
|
87
|
+
throw new ConfigurationError(foreignSubject(source, descriptor.name));
|
|
88
|
+
}
|
|
89
|
+
if (described.has(descriptor.name))
|
|
90
|
+
continue;
|
|
91
|
+
context.registry.register('policies', descriptor);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which subjects a module may speak for (SPEC.md §51, ADR-0027).
|
|
3
|
+
*
|
|
4
|
+
* A policy is a *grant*. `authorize` lets a caller through on a permission **or** a
|
|
5
|
+
* policy, so registering one for `pages` opens `pages.create` to everybody the rule
|
|
6
|
+
* says yes to — and the rule is somebody's function. That is fine while every module
|
|
7
|
+
* in the process is first-party. It stops being fine the day a package is installable,
|
|
8
|
+
* which is what ADR-0027 is for: twelve lines in a dependency would otherwise open the
|
|
9
|
+
* application, and describing the registration (as `assemora.describe` and Studio now
|
|
10
|
+
* do) makes it *visible* rather than *impossible*. Looking is not a control.
|
|
11
|
+
*
|
|
12
|
+
* So a module may only write a policy for a subject it declared. Three ways to have
|
|
13
|
+
* declared one, and each is something the module already says out loud:
|
|
14
|
+
*
|
|
15
|
+
* **Its own name, as a namespace.** `module('pages')` owns `pages` and `pages.drafts`.
|
|
16
|
+
* This is what every framework module relies on — `@assemora/pages` names `pages` as
|
|
17
|
+
* the subject of its block commands, `@assemora/auth` answers for `auth` and
|
|
18
|
+
* `auth.users` — and it is the case where the module name *is* the domain.
|
|
19
|
+
*
|
|
20
|
+
* **A resource or a model it registered.** An application's module is usually named
|
|
21
|
+
* after the area rather than the table: `module('blog').models(Article)
|
|
22
|
+
* .resources(Articles)` owns `articles`, and would own nothing at all under a
|
|
23
|
+
* name-only rule. The registry knows who registered each entry, so this asks it.
|
|
24
|
+
*
|
|
25
|
+
* **The group of a command or query it registered.** A subject is what the authorizer
|
|
26
|
+
* derives from a command's name — everything before the last dot — so a module
|
|
27
|
+
* declaring `menu.list`, `menu.get` and `menu.dish` owns `menu`, and writing a policy
|
|
28
|
+
* for it is writing one for its own reads.
|
|
29
|
+
*
|
|
30
|
+
* That third one is not a convenience. A real project declares exactly those queries
|
|
31
|
+
* and a `policy('menu', …)` beside them, *on purpose*: the menu is public and the
|
|
32
|
+
* generated CRUD over `dishes` is not, so the rule is written on the narrow subject
|
|
33
|
+
* rather than the wide one. Without this clause the only way to keep booting would have
|
|
34
|
+
* been to move the policy onto `dishes` — which opens the admin surface. A rule that
|
|
35
|
+
* pushes an author toward the less safe design is a wrong rule, whatever it prevents.
|
|
36
|
+
*
|
|
37
|
+
* ## What this is not
|
|
38
|
+
*
|
|
39
|
+
* It is not a sandbox. Everything here runs in one process, and a package determined
|
|
40
|
+
* to grant itself access can reach past any of it — patch the module, patch this
|
|
41
|
+
* function, register a resource named `pages` (which the registry would refuse as a
|
|
42
|
+
* duplicate, but the shape of the attack is the point). Nothing short of a separate
|
|
43
|
+
* realm changes that, and claiming otherwise would be worse than saying so.
|
|
44
|
+
*
|
|
45
|
+
* What it removes is the *casual* case, which is the one that actually happens: a
|
|
46
|
+
* package that wants a policy for a subject it has nothing to do with now has to
|
|
47
|
+
* impersonate a module to get one, and impersonation reads as impersonation in a diff.
|
|
48
|
+
*/
|
|
49
|
+
import type { SchemaRegistry } from '@assemora/core';
|
|
50
|
+
/**
|
|
51
|
+
* Whether `module` declared `subject`, and may therefore write a policy for it.
|
|
52
|
+
*
|
|
53
|
+
* @param registry the application's, for the attribution — the module that registered
|
|
54
|
+
* each entry, which is what `forModule` records.
|
|
55
|
+
*/
|
|
56
|
+
export declare const ownsSubject: (registry: SchemaRegistry, module: string, subject: string) => boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Why a subject was refused, in the terms the person reading it can act on.
|
|
59
|
+
*
|
|
60
|
+
* It names all three things the refusal is about — who asked, for what, and what would
|
|
61
|
+
* have had to be true — because a configuration error read at boot is read once, by
|
|
62
|
+
* somebody who did not write the module that caused it.
|
|
63
|
+
*/
|
|
64
|
+
export declare const foreignSubject: (module: string, subject: string) => string;
|
|
65
|
+
/** The same, for a policy that reached the registry through no module at all. */
|
|
66
|
+
export declare const unattributedSubject: (subject: string) => string;
|
|
67
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownership.d.ts","sourceRoot":"","sources":["../src/ownership.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAsBpD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,aAAc,cAAc,UAAU,MAAM,WAAW,MAAM,KAAG,OAYvF,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,WAAY,MAAM,WAAW,MAAM,KAAG,MAM8B,CAAA;AAE/F,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,YAAa,MAAM,KAAG,MAGqC,CAAA"}
|