@pattern-js/mod-identity 0.2.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 +21 -0
- package/README.md +53 -0
- package/dist/admin-routes.d.ts +30 -0
- package/dist/admin-routes.d.ts.map +1 -0
- package/dist/admin-routes.js +52 -0
- package/dist/admin-routes.js.map +1 -0
- package/dist/auth-provider.d.ts +16 -0
- package/dist/auth-provider.d.ts.map +1 -0
- package/dist/auth-provider.js +47 -0
- package/dist/auth-provider.js.map +1 -0
- package/dist/cookies.d.ts +19 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +44 -0
- package/dist/cookies.js.map +1 -0
- package/dist/csrf.d.ts +19 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +39 -0
- package/dist/csrf.js.map +1 -0
- package/dist/deliver.d.ts +28 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +36 -0
- package/dist/deliver.js.map +1 -0
- package/dist/frontend.d.ts +10 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +139 -0
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/mod.d.ts +21 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +97 -0
- package/dist/mod.js.map +1 -0
- package/dist/ops.d.ts +26 -0
- package/dist/ops.d.ts.map +1 -0
- package/dist/ops.js +379 -0
- package/dist/ops.js.map +1 -0
- package/dist/options.d.ts +53 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +22 -0
- package/dist/options.js.map +1 -0
- package/dist/pages/bootstrap.d.ts +13 -0
- package/dist/pages/bootstrap.d.ts.map +1 -0
- package/dist/pages/bootstrap.js +23 -0
- package/dist/pages/bootstrap.js.map +1 -0
- package/dist/pages/html.d.ts +16 -0
- package/dist/pages/html.d.ts.map +1 -0
- package/dist/pages/html.js +72 -0
- package/dist/pages/html.js.map +1 -0
- package/dist/pages/login.d.ts +17 -0
- package/dist/pages/login.d.ts.map +1 -0
- package/dist/pages/login.js +43 -0
- package/dist/pages/login.js.map +1 -0
- package/dist/pages/welcome.d.ts +13 -0
- package/dist/pages/welcome.d.ts.map +1 -0
- package/dist/pages/welcome.js +19 -0
- package/dist/pages/welcome.js.map +1 -0
- package/dist/service-key.d.ts +12 -0
- package/dist/service-key.d.ts.map +1 -0
- package/dist/service-key.js +16 -0
- package/dist/service-key.js.map +1 -0
- package/dist/service.d.ts +133 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +208 -0
- package/dist/service.js.map +1 -0
- package/dist/store/memory.d.ts +11 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +0 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/migrations.d.ts +21 -0
- package/dist/store/migrations.d.ts.map +1 -0
- package/dist/store/migrations.js +87 -0
- package/dist/store/migrations.js.map +1 -0
- package/dist/store/mutex.d.ts +13 -0
- package/dist/store/mutex.d.ts.map +1 -0
- package/dist/store/mutex.js +28 -0
- package/dist/store/mutex.js.map +1 -0
- package/dist/store/sqlite.d.ts +20 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +254 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/types.d.ts +135 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +23 -0
- package/dist/store/types.js.map +1 -0
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +25 -0
- package/dist/tokens.js.map +1 -0
- package/dist/workflows.d.ts +14 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +96 -0
- package/dist/workflows.js.map +1 -0
- package/docs/index.md +162 -0
- package/docs/ops/identity.bootstrap.page.md +5 -0
- package/docs/ops/identity.bootstrap.submit.md +5 -0
- package/docs/ops/identity.login.page.md +5 -0
- package/docs/ops/identity.logout.md +4 -0
- package/docs/ops/identity.sessions.list.md +5 -0
- package/docs/ops/identity.sessions.revoke.md +4 -0
- package/docs/ops/identity.settings.get.md +4 -0
- package/docs/ops/identity.settings.set.md +5 -0
- package/docs/ops/identity.token.callback.md +7 -0
- package/docs/ops/identity.users.get.md +5 -0
- package/docs/ops/identity.users.invite.md +7 -0
- package/docs/ops/identity.users.list.md +5 -0
- package/docs/ops/identity.users.loginLink.md +5 -0
- package/docs/ops/identity.users.revokeSessions.md +5 -0
- package/docs/ops/identity.users.runStats.md +7 -0
- package/docs/ops/identity.users.setRoles.md +5 -0
- package/docs/ops/identity.users.toggleDisabled.md +5 -0
- package/docs/ops/identity.welcome.page.md +5 -0
- package/docs/ops/identity.whoami.md +6 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pattern contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @pattern-js/mod-identity
|
|
2
|
+
|
|
3
|
+
The identity kernel for [Pattern](../../README.md): users, revocable cookie
|
|
4
|
+
sessions, roles→scopes, a single-use token system, the login page, and the
|
|
5
|
+
admin's Access screens (Users / Invite / Sessions). Login *methods* are separate
|
|
6
|
+
mods that plug into it — `@pattern-js/mod-auth-magic-link` is the reference.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @pattern-js/mod-identity
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
Install it the moment you want **users, sessions, or roles** — a login page,
|
|
15
|
+
who-is-this-request, gated routes. It's opt-in: a project without it behaves
|
|
16
|
+
exactly as before. Installing it alongside `@pattern-js/mod-admin` **flips the admin
|
|
17
|
+
to secure-by-default** (`/admin` starts requiring the `admin` scope; pass
|
|
18
|
+
`adminMod({ auth: false })` to keep it open). If you only need a machine-to-machine
|
|
19
|
+
API key you don't need this mod — reach for it when a *human* must prove who they
|
|
20
|
+
are.
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
A login method mod — typically **`@pattern-js/mod-auth-magic-link`**. Identity owns
|
|
25
|
+
the kernel; the method owns the sign-in flow.
|
|
26
|
+
|
|
27
|
+
## Config
|
|
28
|
+
|
|
29
|
+
Defaults work from the bare config entry:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
{ "mods": ["@pattern-js/mod-identity", "@pattern-js/mod-auth-magic-link"] }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To customize, export a local wrapper mod:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// mods/identity.mjs
|
|
39
|
+
import { identityMod } from "@pattern-js/mod-identity";
|
|
40
|
+
export default identityMod({
|
|
41
|
+
signup: "open", // default "invite"
|
|
42
|
+
roles: { admin: ["admin"], editor: ["edit", "read"] },
|
|
43
|
+
cookieSecure: true, // REQUIRED behind TLS
|
|
44
|
+
storage: "./.pattern-data/identity.db", // or "memory"; gitignored
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
On first boot with an empty store, the `ready` hook prints a one-time bootstrap
|
|
49
|
+
link to the server console to create the first admin. Gate routes by stamping
|
|
50
|
+
`requireAuth` on a `boundary.http.request` trigger.
|
|
51
|
+
|
|
52
|
+
Full documentation: the **Identity** chapter at `/docs` (served by
|
|
53
|
+
`@pattern-js/mod-docs`), or [the source](docs/index.md).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — the admin screens' dedicated routes.
|
|
3
|
+
*
|
|
4
|
+
* One purposeful route per screen and action (Users / Sessions / Invite /
|
|
5
|
+
* Settings, plus whoami) — no generic op invoker. Each is a normal workflow
|
|
6
|
+
* (request → extract → identity.* op → status → response) built by the shared
|
|
7
|
+
* `httpEndpoint`, admin-scope-gated at the boundary (the ops also re-check
|
|
8
|
+
* `admin` in-op). `whoami` is the one exception: it reports the caller's own
|
|
9
|
+
* principal, so it needs authentication, not the admin scope. Paths are
|
|
10
|
+
* single-sourced here so the workflows and the manifest can't drift.
|
|
11
|
+
*/
|
|
12
|
+
import { type Workflow } from "@pattern-js/core";
|
|
13
|
+
/** Route paths relative to the admin API mount — the manifest's `RouteRef.path`. */
|
|
14
|
+
export declare const PATHS: {
|
|
15
|
+
readonly users: "/identity/users";
|
|
16
|
+
readonly user: "/identity/users/:userId";
|
|
17
|
+
readonly userRunStats: "/identity/users/:userId/run-stats";
|
|
18
|
+
readonly userSessions: "/identity/users/:userId/sessions";
|
|
19
|
+
readonly userLoginLink: "/identity/users/:userId/login-link";
|
|
20
|
+
readonly userToggleDisabled: "/identity/users/:userId/toggle-disabled";
|
|
21
|
+
readonly userRevokeSessions: "/identity/users/:userId/revoke-sessions";
|
|
22
|
+
readonly invites: "/identity/invites";
|
|
23
|
+
readonly sessions: "/identity/sessions";
|
|
24
|
+
readonly session: "/identity/sessions/:sessionId";
|
|
25
|
+
readonly settings: "/identity/settings";
|
|
26
|
+
readonly whoami: "/identity/whoami";
|
|
27
|
+
};
|
|
28
|
+
/** The route workflows that back the admin Access screens. */
|
|
29
|
+
export declare function identityAdminRoutes(): Workflow[];
|
|
30
|
+
//# sourceMappingURL=admin-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-routes.d.ts","sourceRoot":"","sources":["../src/admin-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAsC,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIrF,oFAAoF;AACpF,eAAO,MAAM,KAAK;;;;;;;;;;;;;CAaR,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,mBAAmB,IAAI,QAAQ,EAAE,CA6BhD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — the admin screens' dedicated routes.
|
|
3
|
+
*
|
|
4
|
+
* One purposeful route per screen and action (Users / Sessions / Invite /
|
|
5
|
+
* Settings, plus whoami) — no generic op invoker. Each is a normal workflow
|
|
6
|
+
* (request → extract → identity.* op → status → response) built by the shared
|
|
7
|
+
* `httpEndpoint`, admin-scope-gated at the boundary (the ops also re-check
|
|
8
|
+
* `admin` in-op). `whoami` is the one exception: it reports the caller's own
|
|
9
|
+
* principal, so it needs authentication, not the admin scope. Paths are
|
|
10
|
+
* single-sourced here so the workflows and the manifest can't drift.
|
|
11
|
+
*/
|
|
12
|
+
import { fromBody, fromParams, httpEndpoint } from "@pattern-js/core";
|
|
13
|
+
const API = "/admin/api";
|
|
14
|
+
/** Route paths relative to the admin API mount — the manifest's `RouteRef.path`. */
|
|
15
|
+
export const PATHS = {
|
|
16
|
+
users: "/identity/users",
|
|
17
|
+
user: "/identity/users/:userId",
|
|
18
|
+
userRunStats: "/identity/users/:userId/run-stats",
|
|
19
|
+
userSessions: "/identity/users/:userId/sessions",
|
|
20
|
+
userLoginLink: "/identity/users/:userId/login-link",
|
|
21
|
+
userToggleDisabled: "/identity/users/:userId/toggle-disabled",
|
|
22
|
+
userRevokeSessions: "/identity/users/:userId/revoke-sessions",
|
|
23
|
+
invites: "/identity/invites",
|
|
24
|
+
sessions: "/identity/sessions",
|
|
25
|
+
session: "/identity/sessions/:sessionId",
|
|
26
|
+
settings: "/identity/settings",
|
|
27
|
+
whoami: "/identity/whoami",
|
|
28
|
+
};
|
|
29
|
+
/** The route workflows that back the admin Access screens. */
|
|
30
|
+
export function identityAdminRoutes() {
|
|
31
|
+
const admin = { scopes: ["admin"] };
|
|
32
|
+
const r = (id, method, path, op, io, auth = admin) => httpEndpoint({ id, name: `Identity · ${method} ${API}${path}`, method, path: `${API}${path}`, op, io, auth });
|
|
33
|
+
return [
|
|
34
|
+
// ── reads ──
|
|
35
|
+
r("identity.route.admin.users.list", "GET", PATHS.users, "identity.users.list", { out: "users" }),
|
|
36
|
+
r("identity.route.admin.users.get", "GET", PATHS.user, "identity.users.get", { in: { userId: fromParams() }, out: "user" }),
|
|
37
|
+
r("identity.route.admin.users.runStats", "GET", PATHS.userRunStats, "identity.users.runStats", { in: { userId: fromParams() }, out: "stats" }),
|
|
38
|
+
r("identity.route.admin.users.sessions", "GET", PATHS.userSessions, "identity.sessions.list", { in: { userId: fromParams() }, out: "sessions" }),
|
|
39
|
+
r("identity.route.admin.sessions.list", "GET", PATHS.sessions, "identity.sessions.list", { out: "sessions" }),
|
|
40
|
+
r("identity.route.admin.settings.get", "GET", PATHS.settings, "identity.settings.get", { out: "settings" }),
|
|
41
|
+
// whoami is about the *caller* — authentication, not the admin scope.
|
|
42
|
+
r("identity.route.admin.whoami", "GET", PATHS.whoami, "identity.whoami", { out: "whoami" }, true),
|
|
43
|
+
// ── actions ──
|
|
44
|
+
r("identity.route.admin.users.invite", "POST", PATHS.invites, "identity.users.invite", { in: { email: fromBody(), roles: fromBody() }, out: "result" }),
|
|
45
|
+
r("identity.route.admin.users.loginLink", "POST", PATHS.userLoginLink, "identity.users.loginLink", { in: { userId: fromParams() }, out: "result" }),
|
|
46
|
+
r("identity.route.admin.users.toggleDisabled", "POST", PATHS.userToggleDisabled, "identity.users.toggleDisabled", { in: { userId: fromParams() }, out: "user" }),
|
|
47
|
+
r("identity.route.admin.users.revokeSessions", "POST", PATHS.userRevokeSessions, "identity.users.revokeSessions", { in: { userId: fromParams() }, out: "result" }),
|
|
48
|
+
r("identity.route.admin.sessions.revoke", "DELETE", PATHS.session, "identity.sessions.revoke", { in: { sessionId: fromParams() }, out: "result" }),
|
|
49
|
+
r("identity.route.admin.settings.set", "POST", PATHS.settings, "identity.settings.set", { in: { signup: fromBody() }, out: "result" }),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=admin-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-routes.js","sourceRoot":"","sources":["../src/admin-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — the session AuthProvider (§9).
|
|
3
|
+
*
|
|
4
|
+
* The bridge from cookie to principal, run by the engine's provider chain on
|
|
5
|
+
* every authenticated surface (HTTP routes, app mounts, WS upgrades — they
|
|
6
|
+
* all call `engine.authenticate`). Returns null fast when the cookie is
|
|
7
|
+
* absent so other providers (API keys, bearer tokens) get their turn.
|
|
8
|
+
*
|
|
9
|
+
* CSRF lives here by design: a cross-site state-changing request simply does
|
|
10
|
+
* not authenticate, so protected writes 401 without any token machinery.
|
|
11
|
+
*/
|
|
12
|
+
import type { AuthProvider } from "@pattern-js/core";
|
|
13
|
+
import type { IdentityService } from "./service.js";
|
|
14
|
+
export declare const SESSION_PROVIDER_NAME = "@pattern-js/mod-identity/session";
|
|
15
|
+
export declare function sessionAuthProvider(svc: () => IdentityService | undefined): AuthProvider;
|
|
16
|
+
//# sourceMappingURL=auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.d.ts","sourceRoot":"","sources":["../src/auth-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAIpD,eAAO,MAAM,qBAAqB,qCAAqC,CAAC;AAExE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,eAAe,GAAG,SAAS,GAAG,YAAY,CA+BxF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — the session AuthProvider (§9).
|
|
3
|
+
*
|
|
4
|
+
* The bridge from cookie to principal, run by the engine's provider chain on
|
|
5
|
+
* every authenticated surface (HTTP routes, app mounts, WS upgrades — they
|
|
6
|
+
* all call `engine.authenticate`). Returns null fast when the cookie is
|
|
7
|
+
* absent so other providers (API keys, bearer tokens) get their turn.
|
|
8
|
+
*
|
|
9
|
+
* CSRF lives here by design: a cross-site state-changing request simply does
|
|
10
|
+
* not authenticate, so protected writes 401 without any token machinery.
|
|
11
|
+
*/
|
|
12
|
+
import { parseCookies } from "./cookies.js";
|
|
13
|
+
import { isCrossSiteWrite, isStateChanging, methodOf } from "./csrf.js";
|
|
14
|
+
export const SESSION_PROVIDER_NAME = "@pattern-js/mod-identity/session";
|
|
15
|
+
export function sessionAuthProvider(svc) {
|
|
16
|
+
return {
|
|
17
|
+
name: SESSION_PROVIDER_NAME,
|
|
18
|
+
async authenticate(ctx) {
|
|
19
|
+
const service = svc();
|
|
20
|
+
if (!service)
|
|
21
|
+
return null;
|
|
22
|
+
const raw = parseCookies(ctx.headers.get("cookie"))[service.options.cookieName];
|
|
23
|
+
if (!raw)
|
|
24
|
+
return null;
|
|
25
|
+
// CSRF: refuse to authenticate cross-site writes (GETs always pass —
|
|
26
|
+
// the magic-link callback is a cross-site top-level GET by nature).
|
|
27
|
+
if (isStateChanging(methodOf(ctx.raw)) && isCrossSiteWrite(ctx.headers))
|
|
28
|
+
return null;
|
|
29
|
+
const resolved = await service.resolveSessionByToken(raw);
|
|
30
|
+
if (!resolved)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
kind: "user",
|
|
34
|
+
id: resolved.user.id,
|
|
35
|
+
provider: SESSION_PROVIDER_NAME,
|
|
36
|
+
scopes: resolved.scopes,
|
|
37
|
+
claims: {
|
|
38
|
+
sessionId: resolved.session.id, // WS hosts join session:{id} with this
|
|
39
|
+
email: resolved.user.email,
|
|
40
|
+
name: resolved.user.name ?? undefined,
|
|
41
|
+
roles: resolved.user.roles,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=auth-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.js","sourceRoot":"","sources":["../src/auth-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAExE,MAAM,CAAC,MAAM,qBAAqB,GAAG,kCAAkC,CAAC;AAExE,MAAM,UAAU,mBAAmB,CAAC,GAAsC;IACxE,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,KAAK,CAAC,YAAY,CAAC,GAAG;YACpB,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAE1B,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAChF,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAC;YAEtB,qEAAqE;YACrE,oEAAoE;YACpE,IAAI,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC;YAErF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC;YAC1D,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAE3B,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE;gBACpB,QAAQ,EAAE,qBAAqB;gBAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,MAAM,EAAE;oBACN,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,uCAAuC;oBACvE,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK;oBAC1B,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,SAAS;oBACrC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK;iBAC3B;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — cookie plumbing.
|
|
3
|
+
*
|
|
4
|
+
* One cookie, handled by hand: HttpOnly always, SameSite=Lax (Strict would
|
|
5
|
+
* drop the cookie on the magic-link's top-level GET redirect back into the
|
|
6
|
+
* app), Path=/, Secure per options.
|
|
7
|
+
*/
|
|
8
|
+
export interface CookieOptions {
|
|
9
|
+
secure: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Parse a Cookie request header into a name → value map. */
|
|
12
|
+
export declare function parseCookies(header: string | null | undefined): Record<string, string>;
|
|
13
|
+
/** Serialize the session cookie (Set-Cookie value). */
|
|
14
|
+
export declare function serializeSessionCookie(name: string, value: string, opts: CookieOptions & {
|
|
15
|
+
maxAgeSeconds: number;
|
|
16
|
+
}): string;
|
|
17
|
+
/** Serialize an immediate-expiry cookie (logout). */
|
|
18
|
+
export declare function clearSessionCookie(name: string, opts: CookieOptions): string;
|
|
19
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWtF;AAED,uDAAuD;AACvD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,aAAa,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAC9C,MAAM,CAUR;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,CAI5E"}
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — cookie plumbing.
|
|
3
|
+
*
|
|
4
|
+
* One cookie, handled by hand: HttpOnly always, SameSite=Lax (Strict would
|
|
5
|
+
* drop the cookie on the magic-link's top-level GET redirect back into the
|
|
6
|
+
* app), Path=/, Secure per options.
|
|
7
|
+
*/
|
|
8
|
+
/** Parse a Cookie request header into a name → value map. */
|
|
9
|
+
export function parseCookies(header) {
|
|
10
|
+
const out = {};
|
|
11
|
+
if (!header)
|
|
12
|
+
return out;
|
|
13
|
+
for (const part of header.split(";")) {
|
|
14
|
+
const eq = part.indexOf("=");
|
|
15
|
+
if (eq === -1)
|
|
16
|
+
continue;
|
|
17
|
+
const name = part.slice(0, eq).trim();
|
|
18
|
+
if (!name)
|
|
19
|
+
continue;
|
|
20
|
+
out[name] = part.slice(eq + 1).trim();
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Serialize the session cookie (Set-Cookie value). */
|
|
25
|
+
export function serializeSessionCookie(name, value, opts) {
|
|
26
|
+
const parts = [
|
|
27
|
+
`${name}=${value}`,
|
|
28
|
+
"Path=/",
|
|
29
|
+
"HttpOnly",
|
|
30
|
+
"SameSite=Lax",
|
|
31
|
+
`Max-Age=${Math.floor(opts.maxAgeSeconds)}`,
|
|
32
|
+
];
|
|
33
|
+
if (opts.secure)
|
|
34
|
+
parts.push("Secure");
|
|
35
|
+
return parts.join("; ");
|
|
36
|
+
}
|
|
37
|
+
/** Serialize an immediate-expiry cookie (logout). */
|
|
38
|
+
export function clearSessionCookie(name, opts) {
|
|
39
|
+
const parts = [`${name}=`, "Path=/", "HttpOnly", "SameSite=Lax", "Max-Age=0"];
|
|
40
|
+
if (opts.secure)
|
|
41
|
+
parts.push("Secure");
|
|
42
|
+
return parts.join("; ");
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,MAAiC;IAC5D,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,KAAK,CAAC,CAAC;YAAE,SAAS;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,sBAAsB,CACpC,IAAY,EACZ,KAAa,EACb,IAA+C;IAE/C,MAAM,KAAK,GAAG;QACZ,GAAG,IAAI,IAAI,KAAK,EAAE;QAClB,QAAQ;QACR,UAAU;QACV,cAAc;QACd,WAAW,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;KAC5C,CAAC;IACF,IAAI,IAAI,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,IAAmB;IAClE,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC;IAC9E,IAAI,IAAI,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/csrf.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — CSRF guard (§9).
|
|
3
|
+
*
|
|
4
|
+
* Lives INSIDE the session auth provider: a cross-site state-changing request
|
|
5
|
+
* simply does not authenticate, so a protected write 401s — no token dance,
|
|
6
|
+
* no second cookie. Fetch Metadata (Sec-Fetch-Site) is authoritative where
|
|
7
|
+
* present; the Origin-vs-Host comparison covers older browsers. Requests
|
|
8
|
+
* without either header (curl, server-to-server) are not browser-CSRF-able
|
|
9
|
+
* and pass.
|
|
10
|
+
*
|
|
11
|
+
* GETs are never blocked — the magic-link callback is by nature a cross-site
|
|
12
|
+
* top-level GET, and safe methods must not mutate anyway.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isStateChanging(method: string): boolean;
|
|
15
|
+
/** Should this state-changing request be refused authentication? */
|
|
16
|
+
export declare function isCrossSiteWrite(headers: Headers): boolean;
|
|
17
|
+
/** Derive the request method from an AuthContext (HTTP req, WS upgrade, …). */
|
|
18
|
+
export declare function methodOf(raw: unknown): string;
|
|
19
|
+
//# sourceMappingURL=csrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAG7C"}
|
package/dist/csrf.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — CSRF guard (§9).
|
|
3
|
+
*
|
|
4
|
+
* Lives INSIDE the session auth provider: a cross-site state-changing request
|
|
5
|
+
* simply does not authenticate, so a protected write 401s — no token dance,
|
|
6
|
+
* no second cookie. Fetch Metadata (Sec-Fetch-Site) is authoritative where
|
|
7
|
+
* present; the Origin-vs-Host comparison covers older browsers. Requests
|
|
8
|
+
* without either header (curl, server-to-server) are not browser-CSRF-able
|
|
9
|
+
* and pass.
|
|
10
|
+
*
|
|
11
|
+
* GETs are never blocked — the magic-link callback is by nature a cross-site
|
|
12
|
+
* top-level GET, and safe methods must not mutate anyway.
|
|
13
|
+
*/
|
|
14
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
15
|
+
export function isStateChanging(method) {
|
|
16
|
+
return !SAFE_METHODS.has(method.toUpperCase());
|
|
17
|
+
}
|
|
18
|
+
/** Should this state-changing request be refused authentication? */
|
|
19
|
+
export function isCrossSiteWrite(headers) {
|
|
20
|
+
const secFetchSite = headers.get("sec-fetch-site");
|
|
21
|
+
if (secFetchSite) {
|
|
22
|
+
return !(secFetchSite === "same-origin" || secFetchSite === "same-site" || secFetchSite === "none");
|
|
23
|
+
}
|
|
24
|
+
const origin = headers.get("origin");
|
|
25
|
+
if (!origin)
|
|
26
|
+
return false; // non-browser client — not CSRF-able
|
|
27
|
+
try {
|
|
28
|
+
return new URL(origin).host !== headers.get("host");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return true; // malformed Origin → refuse
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Derive the request method from an AuthContext (HTTP req, WS upgrade, …). */
|
|
35
|
+
export function methodOf(raw) {
|
|
36
|
+
const m = raw?.method;
|
|
37
|
+
return typeof m === "string" ? m : "GET";
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=csrf.js.map
|
package/dist/csrf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAEzD,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;AACjD,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACnD,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,CAAC,CAAC,YAAY,KAAK,aAAa,IAAI,YAAY,KAAK,WAAW,IAAI,YAAY,KAAK,MAAM,CAAC,CAAC;IACtG,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC,CAAC,qCAAqC;IAChE,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,4BAA4B;IAC3C,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,QAAQ,CAAC,GAAY;IACnC,MAAM,CAAC,GAAI,GAA+C,EAAE,MAAM,CAAC;IACnE,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — token delivery (§ delivery hook).
|
|
3
|
+
*
|
|
4
|
+
* Delivery is a hook chain, not a hardcoded channel: `identity.deliverToken`
|
|
5
|
+
* is invoked with `{ email, url, purpose, delivered: false }` and any
|
|
6
|
+
* workflow subscribed via `boundary.hook` may deliver (email, SMS, chat …)
|
|
7
|
+
* and flip `delivered`. If the payload comes back undelivered, the link is
|
|
8
|
+
* printed to the server console — which is precisely the zero-config dev
|
|
9
|
+
* login, and the same primitive bootstrap uses.
|
|
10
|
+
*/
|
|
11
|
+
import type { OpContext } from "@pattern-js/core";
|
|
12
|
+
export declare const DELIVER_TOKEN_HOOK = "identity.deliverToken";
|
|
13
|
+
export interface DeliverInput {
|
|
14
|
+
email: string;
|
|
15
|
+
/** Path-only callback URL from `issueToken` (e.g. "/auth/token?t=…"). */
|
|
16
|
+
path: string;
|
|
17
|
+
purpose: string;
|
|
18
|
+
/** The request origin (e.g. "http://localhost:3000"), when available — makes the link absolute. */
|
|
19
|
+
origin?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface DeliverResult {
|
|
22
|
+
delivered: boolean;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
/** Best-effort absolute URL — `origin` (from the trigger's `url`) + the callback path. */
|
|
26
|
+
export declare function absoluteUrl(path: string, origin?: string | null): string;
|
|
27
|
+
export declare function deliverToken(ctx: OpContext, input: DeliverInput): Promise<DeliverResult>;
|
|
28
|
+
//# sourceMappingURL=deliver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../src/deliver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAExE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAoB9F"}
|
package/dist/deliver.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — token delivery (§ delivery hook).
|
|
3
|
+
*
|
|
4
|
+
* Delivery is a hook chain, not a hardcoded channel: `identity.deliverToken`
|
|
5
|
+
* is invoked with `{ email, url, purpose, delivered: false }` and any
|
|
6
|
+
* workflow subscribed via `boundary.hook` may deliver (email, SMS, chat …)
|
|
7
|
+
* and flip `delivered`. If the payload comes back undelivered, the link is
|
|
8
|
+
* printed to the server console — which is precisely the zero-config dev
|
|
9
|
+
* login, and the same primitive bootstrap uses.
|
|
10
|
+
*/
|
|
11
|
+
export const DELIVER_TOKEN_HOOK = "identity.deliverToken";
|
|
12
|
+
/** Best-effort absolute URL — `origin` (from the trigger's `url`) + the callback path. */
|
|
13
|
+
export function absoluteUrl(path, origin) {
|
|
14
|
+
return origin ? `${origin.replace(/\/$/, "")}${path}` : path;
|
|
15
|
+
}
|
|
16
|
+
export async function deliverToken(ctx, input) {
|
|
17
|
+
const url = absoluteUrl(input.path, input.origin);
|
|
18
|
+
const payload = { email: input.email, url, purpose: input.purpose, delivered: false };
|
|
19
|
+
let result;
|
|
20
|
+
try {
|
|
21
|
+
result = await ctx.services.hooks.invoke(DELIVER_TOKEN_HOOK, payload);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
// A broken delivery workflow must never eat the login link.
|
|
25
|
+
ctx.log("warn", `deliverToken hook failed; falling back to console: ${err.message}`);
|
|
26
|
+
result = payload;
|
|
27
|
+
}
|
|
28
|
+
const delivered = Boolean(result?.delivered);
|
|
29
|
+
if (!delivered) {
|
|
30
|
+
console.log(`\n[pattern] ✉ ${input.purpose} link for ${input.email}\n` +
|
|
31
|
+
`[pattern] ${url}\n` +
|
|
32
|
+
`[pattern] (deliver these yourself by subscribing a workflow to the "${DELIVER_TOKEN_HOOK}" hook)\n`);
|
|
33
|
+
}
|
|
34
|
+
return { delivered, url };
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=deliver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.js","sourceRoot":"","sources":["../src/deliver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC;AAgB1D,0FAA0F;AAC1F,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,MAAsB;IAC9D,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAc,EAAE,KAAmB;IACpE,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACtF,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IACxE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,4DAA4D;QAC5D,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,sDAAuD,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAChG,MAAM,GAAG,OAAO,CAAC;IACnB,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAE,MAA8C,EAAE,SAAS,CAAC,CAAC;IACtF,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CACT,iBAAiB,KAAK,CAAC,OAAO,aAAa,KAAK,CAAC,KAAK,IAAI;YACxD,eAAe,GAAG,IAAI;YACtB,yEAAyE,kBAAkB,WAAW,CACzG,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — admin screens (Tier-1 declarative, zero build).
|
|
3
|
+
*
|
|
4
|
+
* Users / Invite / Sessions under an "Access" category. Every view and action
|
|
5
|
+
* names its own dedicated route (see `./admin-routes.ts`) — there is no generic
|
|
6
|
+
* op invoker; the routes are admin-scope-gated and the ops re-check scopes too.
|
|
7
|
+
*/
|
|
8
|
+
import type { FrontendContribution } from "@pattern-js/core";
|
|
9
|
+
export declare function identityFrontend(): FrontendContribution;
|
|
10
|
+
//# sourceMappingURL=frontend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend.d.ts","sourceRoot":"","sources":["../src/frontend.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,wBAAgB,gBAAgB,IAAI,oBAAoB,CAiIvD"}
|
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pattern-js/mod-identity — admin screens (Tier-1 declarative, zero build).
|
|
3
|
+
*
|
|
4
|
+
* Users / Invite / Sessions under an "Access" category. Every view and action
|
|
5
|
+
* names its own dedicated route (see `./admin-routes.ts`) — there is no generic
|
|
6
|
+
* op invoker; the routes are admin-scope-gated and the ops re-check scopes too.
|
|
7
|
+
*/
|
|
8
|
+
import { PATHS } from "./admin-routes.js";
|
|
9
|
+
export function identityFrontend() {
|
|
10
|
+
return {
|
|
11
|
+
menu: [
|
|
12
|
+
{ category: "Access", label: "Users", icon: "users", path: "/x/identity/users", order: 10 },
|
|
13
|
+
{ category: "Access", label: "Invite", icon: "user-plus", path: "/x/identity/invite", order: 20 },
|
|
14
|
+
{ category: "Access", label: "Sessions", icon: "key-round", path: "/x/identity/sessions", order: 30 },
|
|
15
|
+
],
|
|
16
|
+
pages: [
|
|
17
|
+
{
|
|
18
|
+
path: "/x/identity/users",
|
|
19
|
+
view: {
|
|
20
|
+
kind: "table",
|
|
21
|
+
route: { method: "GET", path: PATHS.users },
|
|
22
|
+
columns: [
|
|
23
|
+
{ key: "email", label: "Email" },
|
|
24
|
+
{ key: "name", label: "Name" },
|
|
25
|
+
{ key: "roles", label: "Roles" },
|
|
26
|
+
{ key: "disabled", label: "Disabled", format: "badge" },
|
|
27
|
+
{ key: "created", label: "Created", format: "date" },
|
|
28
|
+
],
|
|
29
|
+
rowActions: [
|
|
30
|
+
// `path` navigates (tokens filled from the row); result:"show" —
|
|
31
|
+
// the minted link is FOR the operator (copyable); the mutations
|
|
32
|
+
// stay silent: the refreshed row is the feedback.
|
|
33
|
+
{ label: "Details", path: "/x/identity/users/:userId", args: { userId: "id" }, icon: "user" },
|
|
34
|
+
{ label: "Sign-in link", route: { method: "POST", path: PATHS.userLoginLink }, args: { userId: "id" }, icon: "key-round", result: "show" },
|
|
35
|
+
{ label: "Toggle disabled", route: { method: "POST", path: PATHS.userToggleDisabled }, args: { userId: "id" }, icon: "ban", confirm: true },
|
|
36
|
+
{ label: "Log out everywhere", route: { method: "POST", path: PATHS.userRevokeSessions }, args: { userId: "id" }, icon: "log-out", confirm: true },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
// The user details page: profile + run metrics, three declarative views
|
|
42
|
+
// over dedicated routes — the :userId param fills each route's path.
|
|
43
|
+
path: "/x/identity/users/:userId",
|
|
44
|
+
views: [
|
|
45
|
+
{ title: "Profile", view: { kind: "detail", route: { method: "GET", path: PATHS.user } } },
|
|
46
|
+
{
|
|
47
|
+
title: "Runs by workflow (recent window)",
|
|
48
|
+
view: {
|
|
49
|
+
kind: "table",
|
|
50
|
+
route: { method: "GET", path: PATHS.userRunStats },
|
|
51
|
+
columns: [
|
|
52
|
+
{ key: "workflow", label: "Workflow" },
|
|
53
|
+
{ key: "runs", label: "Runs" },
|
|
54
|
+
{ key: "errors", label: "Errors" },
|
|
55
|
+
{ key: "avg ms", label: "Avg ms" },
|
|
56
|
+
{ key: "last run", label: "Last run" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "Sessions",
|
|
62
|
+
view: {
|
|
63
|
+
kind: "table",
|
|
64
|
+
route: { method: "GET", path: PATHS.userSessions },
|
|
65
|
+
columns: [
|
|
66
|
+
{ key: "status", label: "Status", format: "badge" },
|
|
67
|
+
{ key: "created", label: "Created", format: "date" },
|
|
68
|
+
{ key: "lastSeen", label: "Last seen", format: "date" },
|
|
69
|
+
{ key: "userAgent", label: "Device" },
|
|
70
|
+
],
|
|
71
|
+
rowActions: [
|
|
72
|
+
{ label: "Revoke", route: { method: "DELETE", path: PATHS.session }, args: { sessionId: "id" }, icon: "log-out", confirm: true },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
path: "/x/identity/invite",
|
|
80
|
+
view: {
|
|
81
|
+
kind: "form",
|
|
82
|
+
schema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
email: { type: "string", description: "Who to invite" },
|
|
86
|
+
roles: { type: "string", description: 'Comma-separated roles, e.g. "admin" (empty = plain user)' },
|
|
87
|
+
},
|
|
88
|
+
required: ["email"],
|
|
89
|
+
},
|
|
90
|
+
route: { method: "POST", path: PATHS.invites },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: "/x/identity/sessions",
|
|
95
|
+
view: {
|
|
96
|
+
kind: "table",
|
|
97
|
+
route: { method: "GET", path: PATHS.sessions },
|
|
98
|
+
columns: [
|
|
99
|
+
{ key: "user", label: "User" },
|
|
100
|
+
{ key: "status", label: "Status", format: "badge" },
|
|
101
|
+
{ key: "created", label: "Created", format: "date" },
|
|
102
|
+
{ key: "lastSeen", label: "Last seen", format: "date" },
|
|
103
|
+
{ key: "userAgent", label: "Device" },
|
|
104
|
+
],
|
|
105
|
+
rowActions: [
|
|
106
|
+
{ label: "Revoke", route: { method: "DELETE", path: PATHS.session }, args: { sessionId: "id" }, icon: "log-out", confirm: true },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
commands: [
|
|
112
|
+
{ id: "identity.invite", label: "Invite user…", group: "Access", icon: "user-plus", path: "/x/identity/invite" },
|
|
113
|
+
{ id: "identity.whoami", label: "Who am I?", group: "Access", icon: "user", route: { method: "GET", path: PATHS.whoami } },
|
|
114
|
+
],
|
|
115
|
+
// Lives on the admin's Settings page (System → Settings), with the other knobs.
|
|
116
|
+
settings: [
|
|
117
|
+
{
|
|
118
|
+
id: "identity",
|
|
119
|
+
title: "Identity",
|
|
120
|
+
description: "Who may sign up. Invites and admin-minted sign-in links work in both modes.",
|
|
121
|
+
route: { method: "GET", path: PATHS.settings },
|
|
122
|
+
submitRoute: { method: "POST", path: PATHS.settings },
|
|
123
|
+
fields: [
|
|
124
|
+
{
|
|
125
|
+
key: "signup",
|
|
126
|
+
label: "Sign-ups",
|
|
127
|
+
type: "select",
|
|
128
|
+
options: [
|
|
129
|
+
{ value: "invite", label: "Invite-only" },
|
|
130
|
+
{ value: "open", label: "Open" },
|
|
131
|
+
],
|
|
132
|
+
description: "Invite-only: unknown emails get no magic link and can't create accounts.",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=frontend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend.js","sourceRoot":"","sources":["../src/frontend.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,IAAI,EAAE;YACJ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,EAAE;YAC3F,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE,EAAE;YACjG,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,sBAAsB,EAAE,KAAK,EAAE,EAAE,EAAE;SACtG;QACD,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,mBAAmB;gBACzB,IAAI,EAAE;oBACJ,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE;oBAC3C,OAAO,EAAE;wBACP,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;wBAChC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;wBAC9B,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;wBAChC,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE;wBACvD,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE;qBACrD;oBACD,UAAU,EAAE;wBACV,iEAAiE;wBACjE,gEAAgE;wBAChE,kDAAkD;wBAClD,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,2BAA2B,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;wBAC7F,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE;wBAC1I,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;wBAC3I,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,kBAAkB,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;qBACnJ;iBACF;aACF;YACD;gBACE,wEAAwE;gBACxE,qEAAqE;gBACrE,IAAI,EAAE,2BAA2B;gBACjC,KAAK,EAAE;oBACL,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE;oBAC1F;wBACE,KAAK,EAAE,kCAAkC;wBACzC,IAAI,EAAE;4BACJ,IAAI,EAAE,OAAO;4BACb,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,EAAE;4BAClD,OAAO,EAAE;gCACP,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;gCACtC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;gCAC9B,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE;gCAClC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE;gCAClC,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE;6BACvC;yBACF;qBACF;oBACD;wBACE,KAAK,EAAE,UAAU;wBACjB,IAAI,EAAE;4BACJ,IAAI,EAAE,OAAO;4BACb,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,EAAE;4BAClD,OAAO,EAAE;gCACP,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;gCACnD,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE;gCACpD,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE;gCACvD,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE;6BACtC;4BACD,UAAU,EAAE;gCACV,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;6BACjI;yBACF;qBACF;iBACF;aACF;YACD;gBACE,IAAI,EAAE,oBAAoB;gBAC1B,IAAI,EAAE;oBACJ,IAAI,EAAE,MAAM;oBACZ,MAAM,EAAE;wBACN,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE;4BACvD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0DAA0D,EAAE;yBACnG;wBACD,QAAQ,EAAE,CAAC,OAAO,CAAC;qBACpB;oBACD,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE;iBAC/C;aACF;YACD;gBACE,IAAI,EAAE,sBAAsB;gBAC5B,IAAI,EAAE;oBACJ,IAAI,EAAE,OAAO;oBACb,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE;oBAC9C,OAAO,EAAE;wBACP,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;wBAC9B,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;wBACnD,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE;wBACpD,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE;wBACvD,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE;qBACtC;oBACD,UAAU,EAAE;wBACV,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE;qBACjI;iBACF;aACF;SACF;QACD,QAAQ,EAAE;YACR,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,EAAE;YAChH,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE;SAC3H;QACD,gFAAgF;QAChF,QAAQ,EAAE;YACR;gBACE,EAAE,EAAE,UAAU;gBACd,KAAK,EAAE,UAAU;gBACjB,WAAW,EAAE,6EAA6E;gBAC1F,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE;gBAC9C,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE;gBACrD,MAAM,EAAE;oBACN;wBACE,GAAG,EAAE,QAAQ;wBACb,KAAK,EAAE,UAAU;wBACjB,IAAI,EAAE,QAAQ;wBACd,OAAO,EAAE;4BACP,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE;4BACzC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;yBACjC;wBACD,WAAW,EAAE,0EAA0E;qBACxF;iBACF;aACF;SACF;KACF,CAAC;AACJ,CAAC"}
|