@better-forge/auth 0.0.0-stage → 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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -3
  3. package/dist/apiTokenRoutes.d.ts +8 -0
  4. package/dist/apiTokenRoutes.d.ts.map +1 -0
  5. package/dist/apiTokenRoutes.js +67 -0
  6. package/dist/apiTokenRoutes.js.map +1 -0
  7. package/dist/apiTokens.d.ts +25 -0
  8. package/dist/apiTokens.d.ts.map +1 -0
  9. package/dist/apiTokens.js +50 -0
  10. package/dist/apiTokens.js.map +1 -0
  11. package/dist/authRoutes.d.ts +37 -0
  12. package/dist/authRoutes.d.ts.map +1 -0
  13. package/dist/authRoutes.js +403 -0
  14. package/dist/authRoutes.js.map +1 -0
  15. package/dist/context.d.ts +98 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +2 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/createAuth.d.ts +146 -0
  20. package/dist/createAuth.d.ts.map +1 -0
  21. package/dist/createAuth.js +52 -0
  22. package/dist/createAuth.js.map +1 -0
  23. package/dist/identityConfirmation.d.ts +13 -0
  24. package/dist/identityConfirmation.d.ts.map +1 -0
  25. package/dist/identityConfirmation.js +37 -0
  26. package/dist/identityConfirmation.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/middleware.d.ts +41 -0
  32. package/dist/middleware.d.ts.map +1 -0
  33. package/dist/middleware.js +100 -0
  34. package/dist/middleware.js.map +1 -0
  35. package/dist/migrations.d.ts +4 -0
  36. package/dist/migrations.d.ts.map +1 -0
  37. package/dist/migrations.js +77 -0
  38. package/dist/migrations.js.map +1 -0
  39. package/dist/module.d.ts +8 -0
  40. package/dist/module.d.ts.map +1 -0
  41. package/dist/module.js +5 -0
  42. package/dist/module.js.map +1 -0
  43. package/dist/oidc.d.ts +85 -0
  44. package/dist/oidc.d.ts.map +1 -0
  45. package/dist/oidc.js +213 -0
  46. package/dist/oidc.js.map +1 -0
  47. package/dist/oidcConfig.d.ts +39 -0
  48. package/dist/oidcConfig.d.ts.map +1 -0
  49. package/dist/oidcConfig.js +72 -0
  50. package/dist/oidcConfig.js.map +1 -0
  51. package/dist/oidcSettings.d.ts +15 -0
  52. package/dist/oidcSettings.d.ts.map +1 -0
  53. package/dist/oidcSettings.js +51 -0
  54. package/dist/oidcSettings.js.map +1 -0
  55. package/dist/oidcSettingsRoutes.d.ts +19 -0
  56. package/dist/oidcSettingsRoutes.d.ts.map +1 -0
  57. package/dist/oidcSettingsRoutes.js +133 -0
  58. package/dist/oidcSettingsRoutes.js.map +1 -0
  59. package/dist/roles.d.ts +25 -0
  60. package/dist/roles.d.ts.map +1 -0
  61. package/dist/roles.js +44 -0
  62. package/dist/roles.js.map +1 -0
  63. package/dist/routeScope.d.ts +34 -0
  64. package/dist/routeScope.d.ts.map +1 -0
  65. package/dist/routeScope.js +32 -0
  66. package/dist/routeScope.js.map +1 -0
  67. package/dist/sessions.d.ts +46 -0
  68. package/dist/sessions.d.ts.map +1 -0
  69. package/dist/sessions.js +159 -0
  70. package/dist/sessions.js.map +1 -0
  71. package/dist/setupRoutes.d.ts +37 -0
  72. package/dist/setupRoutes.d.ts.map +1 -0
  73. package/dist/setupRoutes.js +70 -0
  74. package/dist/setupRoutes.js.map +1 -0
  75. package/dist/signInAudit.d.ts +19 -0
  76. package/dist/signInAudit.d.ts.map +1 -0
  77. package/dist/signInAudit.js +16 -0
  78. package/dist/signInAudit.js.map +1 -0
  79. package/dist/tables.d.ts +716 -0
  80. package/dist/tables.d.ts.map +1 -0
  81. package/dist/tables.js +56 -0
  82. package/dist/tables.js.map +1 -0
  83. package/dist/tokenCatalogue.d.ts +21 -0
  84. package/dist/tokenCatalogue.d.ts.map +1 -0
  85. package/dist/tokenCatalogue.js +27 -0
  86. package/dist/tokenCatalogue.js.map +1 -0
  87. package/dist/totp.d.ts +12 -0
  88. package/dist/totp.d.ts.map +1 -0
  89. package/dist/totp.js +93 -0
  90. package/dist/totp.js.map +1 -0
  91. package/dist/twoFactor.d.ts +10 -0
  92. package/dist/twoFactor.d.ts.map +1 -0
  93. package/dist/twoFactor.js +43 -0
  94. package/dist/twoFactor.js.map +1 -0
  95. package/dist/userRoutes.d.ts +13 -0
  96. package/dist/userRoutes.d.ts.map +1 -0
  97. package/dist/userRoutes.js +135 -0
  98. package/dist/userRoutes.js.map +1 -0
  99. package/package.json +68 -6
  100. package/src/apiTokenRoutes.ts +82 -0
  101. package/src/apiTokens.ts +62 -0
  102. package/src/authRoutes.ts +452 -0
  103. package/src/context.ts +85 -0
  104. package/src/createAuth.ts +58 -0
  105. package/src/identityConfirmation.ts +50 -0
  106. package/src/index.ts +17 -0
  107. package/src/middleware.ts +123 -0
  108. package/src/migrations.ts +77 -0
  109. package/src/module.ts +6 -0
  110. package/src/oidc.ts +291 -0
  111. package/src/oidcConfig.ts +96 -0
  112. package/src/oidcSettings.ts +60 -0
  113. package/src/oidcSettingsRoutes.ts +165 -0
  114. package/src/roles.ts +60 -0
  115. package/src/routeScope.ts +50 -0
  116. package/src/sessions.ts +209 -0
  117. package/src/setupRoutes.ts +100 -0
  118. package/src/signInAudit.ts +43 -0
  119. package/src/tables.ts +59 -0
  120. package/src/tokenCatalogue.ts +42 -0
  121. package/src/totp.ts +96 -0
  122. package/src/twoFactor.ts +49 -0
  123. package/src/userRoutes.ts +144 -0
@@ -0,0 +1,133 @@
1
+ import { oidcSettings } from './tables.js';
2
+ import { requestIdentity } from './middleware.js';
3
+ export function createOidcSettingsRoutes(ctx, parts) {
4
+ const { db, audit, secretBox } = ctx;
5
+ const { confirmIdentity } = parts.identityConfirmation;
6
+ const { readOidcTestResult, storeOidcTestResult, testOidcDiscovery } = parts.oidc;
7
+ const { getOidcConfig, getOidcSettingsRow, getOidcTestConfig, oidcConfigFromRow } = parts.oidcSettings;
8
+ const { buildOidcConfig, defaultOidcRedirectUri } = parts.resolver;
9
+ const { startOidcFlow } = parts;
10
+ function publicView(row) {
11
+ return {
12
+ enabled: !!row?.enabled,
13
+ issuer: row?.issuer ?? '',
14
+ clientId: row?.clientId ?? '',
15
+ hasClientSecret: !!row?.clientSecret,
16
+ scopes: row?.scopes ?? '',
17
+ redirectUri: row?.redirectUri ?? '',
18
+ buttonLabel: row?.buttonLabel ?? '',
19
+ allowUnverifiedEmail: !!row?.allowUnverifiedEmail,
20
+ disablePasswordLogin: !!row?.disablePasswordLogin,
21
+ };
22
+ }
23
+ /** Fields that appear in the audit diff; the secret is only recorded as changed, never its value. */
24
+ function auditView(row, secretChanged = false) {
25
+ const { hasClientSecret: _has, ...rest } = publicView(row);
26
+ return { ...rest, clientSecretChanged: secretChanged };
27
+ }
28
+ async function discoveryError(config) {
29
+ try {
30
+ await testOidcDiscovery(config);
31
+ return null;
32
+ }
33
+ catch (error) {
34
+ return error instanceof Error ? error.message : 'Discovery failed';
35
+ }
36
+ }
37
+ async function oidcSettingsRoutes(app) {
38
+ app.get('/', async () => {
39
+ const [row, active] = await Promise.all([getOidcSettingsRow(), getOidcConfig()]);
40
+ return {
41
+ source: active.source,
42
+ envManaged: active.source === 'env',
43
+ defaultRedirectUri: defaultOidcRedirectUri(),
44
+ settings: publicView(row),
45
+ };
46
+ });
47
+ app.put('/', { config: { rateLimit: ctx.rateLimits.sensitiveAction } }, async (req, reply) => {
48
+ if ((await getOidcConfig()).source === 'env') {
49
+ return reply.code(409).send({ error: 'OIDC is configured through environment variables and cannot be changed here' });
50
+ }
51
+ const body = req.body ?? {};
52
+ const identity = requestIdentity(req);
53
+ if (!await confirmIdentity(identity, body.currentPassword, reply))
54
+ return reply;
55
+ const previous = await getOidcSettingsRow();
56
+ const secretChanged = !!body.clientSecret?.trim();
57
+ // The stored secret is sent to the issuer's token endpoint, so it must not follow a changed target.
58
+ if (previous?.clientSecret && !secretChanged && ((body.issuer ?? '').trim() !== previous.issuer || (body.clientId ?? '').trim() !== previous.clientId)) {
59
+ return reply.code(400).send({ error: 'Re-enter the client secret when changing the issuer URL or client ID' });
60
+ }
61
+ const clientSecret = secretChanged ? secretBox.encrypt(body.clientSecret.trim()) : previous?.clientSecret ?? '';
62
+ const next = {
63
+ id: 1,
64
+ enabled: body.enabled ? 1 : 0,
65
+ issuer: (body.issuer ?? '').trim(),
66
+ clientId: (body.clientId ?? '').trim(),
67
+ clientSecret,
68
+ scopes: (body.scopes ?? '').trim(),
69
+ redirectUri: (body.redirectUri ?? '').trim(),
70
+ buttonLabel: (body.buttonLabel ?? '').trim(),
71
+ allowUnverifiedEmail: body.allowUnverifiedEmail ? 1 : 0,
72
+ disablePasswordLogin: body.disablePasswordLogin ? 1 : 0,
73
+ updatedAt: Date.now(),
74
+ };
75
+ if (next.enabled) {
76
+ const config = oidcConfigFromRow(next);
77
+ if (!config) {
78
+ return reply.code(400).send({ error: 'Issuer URL, client ID and a redirect URI (or PUBLIC_URL) are required and must be valid http(s) URLs' });
79
+ }
80
+ // Never persist a setup that could lock everyone out: with password login off, the IdP must be reachable.
81
+ if (config.disablePasswordLogin) {
82
+ const failure = await discoveryError(config);
83
+ if (failure)
84
+ return reply.code(400).send({ error: `Password sign-in can only be disabled after OIDC discovery succeeds: ${failure}` });
85
+ }
86
+ }
87
+ else {
88
+ next.disablePasswordLogin = 0;
89
+ }
90
+ await db.insert(oidcSettings).values(next).onConflictDoUpdate({ target: oidcSettings.id, set: { ...next } });
91
+ await audit.write(audit.actor(identity), previous ? 'update' : 'create', 'oidc_settings', 1, 'OIDC sign-in', audit.diffObjects(auditView(previous), auditView(next, secretChanged)));
92
+ return publicView(next);
93
+ });
94
+ // Opened in a new tab: runs the real sign-in with the saved settings and shows the outcome instead of signing in.
95
+ app.get('/test-sign-in', { config: { rateLimit: ctx.rateLimits.sensitiveAction } }, async (req, reply) => {
96
+ const cfg = await getOidcTestConfig();
97
+ if (!cfg)
98
+ return reply.code(400).send({ error: 'Save a valid issuer URL, client ID and redirect URI first' });
99
+ try {
100
+ return await startOidcFlow(app, reply, cfg, { purpose: 'oidc-test', adminUserId: requestIdentity(req).userId });
101
+ }
102
+ catch (error) {
103
+ const result = {
104
+ issuer: cfg.issuer, testedAt: Date.now(), claims: null, outcome: 'deny', user: null,
105
+ denial: { code: 'discovery_failed', reason: `OIDC discovery against the issuer failed: ${error instanceof Error ? error.message : String(error)}` },
106
+ };
107
+ return reply.redirect(`${ctx.urls.ssoTest}?result=${storeOidcTestResult(requestIdentity(req).userId, result)}`);
108
+ }
109
+ });
110
+ app.get('/test-sign-in/:id', async (req, reply) => {
111
+ const result = readOidcTestResult(req.params.id, requestIdentity(req).userId);
112
+ if (!result)
113
+ return reply.code(404).send({ error: 'The test result has expired. Run the test sign-in again.' });
114
+ return result;
115
+ });
116
+ app.post('/test', { config: { rateLimit: ctx.rateLimits.sensitiveAction } }, async (req, reply) => {
117
+ const body = req.body ?? {};
118
+ const config = buildOidcConfig({
119
+ issuer: body.issuer,
120
+ clientId: body.clientId,
121
+ redirectUri: body.redirectUri,
122
+ });
123
+ if (!config)
124
+ return reply.code(400).send({ error: 'Enter a valid issuer URL, client ID and a redirect URI (or set PUBLIC_URL)' });
125
+ const failure = await discoveryError(config);
126
+ if (failure)
127
+ return reply.code(400).send({ error: `Discovery failed: ${failure}` });
128
+ return { ok: true, ...(await testOidcDiscovery(config)) };
129
+ });
130
+ }
131
+ return { oidcSettingsRoutes };
132
+ }
133
+ //# sourceMappingURL=oidcSettingsRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidcSettingsRoutes.js","sourceRoot":"","sources":["../src/oidcSettingsRoutes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /** The role that may do everything; it exists in every application. */
2
+ export declare const ADMIN_ROLE = "admin";
3
+ export interface RoleRegistry {
4
+ /**
5
+ * Adds the application's roles to `admin`. `fallback` is what a stored value that is not a role normalises to,
6
+ * and `defaultForNewUsers` is the role an account gets when none is asked for.
7
+ */
8
+ register(roles: readonly string[], options: {
9
+ fallback: string;
10
+ defaultForNewUsers: string;
11
+ }): void;
12
+ /** Normalizes a stored role value, including the legacy JSON-array format. */
13
+ normalize(raw: string): string;
14
+ isValid(role: string): boolean;
15
+ /** Every role, `admin` first, in the order they were registered. */
16
+ list(): readonly string[];
17
+ readonly defaultForNewUsers: string;
18
+ /**
19
+ * Called by createAuth: after this, registering throws, so the set of roles cannot change under a running app.
20
+ * It also throws when the application never registered its roles, because the fallback would then be `admin`.
21
+ */
22
+ freeze(): void;
23
+ }
24
+ export declare function createRoleRegistry(): RoleRegistry;
25
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,eAAO,MAAM,UAAU,UAAU,CAAA;AAEjC,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACnG,8EAA8E;IAC9E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,oEAAoE;IACpE,IAAI,IAAI,SAAS,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IACnC;;;OAGG;IACH,MAAM,IAAI,IAAI,CAAA;CACf;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAqCjD"}
package/dist/roles.js ADDED
@@ -0,0 +1,44 @@
1
+ /** The role that may do everything; it exists in every application. */
2
+ export const ADMIN_ROLE = 'admin';
3
+ export function createRoleRegistry() {
4
+ const roles = [ADMIN_ROLE];
5
+ let fallback = ADMIN_ROLE;
6
+ let defaultForNewUsers = ADMIN_ROLE;
7
+ let frozen = false;
8
+ let registered = false;
9
+ const isValid = (role) => roles.includes(role);
10
+ return {
11
+ register(added, options) {
12
+ if (frozen)
13
+ throw new Error('Roles cannot be registered after the auth module was created');
14
+ for (const role of added)
15
+ if (!roles.includes(role))
16
+ roles.push(role);
17
+ if (!roles.includes(options.fallback) || !roles.includes(options.defaultForNewUsers)) {
18
+ throw new Error('The fallback and the default role for new users must be registered roles');
19
+ }
20
+ fallback = options.fallback;
21
+ defaultForNewUsers = options.defaultForNewUsers;
22
+ registered = true;
23
+ },
24
+ normalize(raw) {
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ const role = Array.isArray(parsed) ? parsed[0] : raw;
28
+ return typeof role === 'string' && isValid(role) ? role : fallback;
29
+ }
30
+ catch {
31
+ return isValid(raw) ? raw : fallback;
32
+ }
33
+ },
34
+ isValid,
35
+ list: () => roles,
36
+ get defaultForNewUsers() { return defaultForNewUsers; },
37
+ freeze() {
38
+ if (!registered)
39
+ throw new Error('Register the application roles before creating the auth module: without them every unknown role would be an administrator');
40
+ frozen = true;
41
+ },
42
+ };
43
+ }
44
+ //# sourceMappingURL=roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.js","sourceRoot":"","sources":["../src/roles.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAA;AAqBjC,MAAM,UAAU,kBAAkB;IAChC,MAAM,KAAK,GAAa,CAAC,UAAU,CAAC,CAAA;IACpC,IAAI,QAAQ,GAAW,UAAU,CAAA;IACjC,IAAI,kBAAkB,GAAW,UAAU,CAAA;IAC3C,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,IAAI,UAAU,GAAG,KAAK,CAAA;IAEtB,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IAEtD,OAAO;QACL,QAAQ,CAAC,KAAK,EAAE,OAAO;YACrB,IAAI,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAA;YAC3F,KAAK,MAAM,IAAI,IAAI,KAAK;gBAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACrE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBACrF,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAA;YAC7F,CAAC;YACD,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;YAC3B,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAA;YAC/C,UAAU,GAAG,IAAI,CAAA;QACnB,CAAC;QACD,SAAS,CAAC,GAAG;YACX,IAAI,CAAC;gBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBACvC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;gBACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAA;YACpE,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAA;YACtC,CAAC;QACH,CAAC;QACD,OAAO;QACP,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK;QACjB,IAAI,kBAAkB,KAAK,OAAO,kBAAkB,CAAA,CAAC,CAAC;QACtD,MAAM;YACJ,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,KAAK,CAAC,2HAA2H,CAAC,CAAA;YAC7J,MAAM,GAAG,IAAI,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ /**
3
+ * Marks every route registered in `scope` as belonging to a part of the API, which is what an API token is checked
4
+ * against (`<part>:read` for GET, `<part>:write` otherwise). A route without a part is refused to every token.
5
+ */
6
+ export declare function belongsTo(scope: FastifyInstance, part: string): void;
7
+ /** Lets (or, with `false`, stops) the routes registered in `scope` accept an API token. Routes accept none by default. */
8
+ export declare function acceptApiTokens(scope: FastifyInstance, accept?: boolean): void;
9
+ type RoutePlugin = (scope: FastifyInstance) => Promise<void> | void;
10
+ type Guard = (...args: never[]) => unknown;
11
+ /** A group of routes that share a guard and, usually, a part of the API tokens are checked against. */
12
+ export interface AdminRouteGroup {
13
+ /** The part of the API the routes belong to. Omit it for a group whose routes no token reaches. */
14
+ part?: string;
15
+ /** The group refuses API tokens even though the rest of the API accepts them. */
16
+ noTokens?: boolean;
17
+ /** Runs before every route of the group (the role the group needs). */
18
+ guard: Guard;
19
+ /** The route plugins of the group with the prefix each is mounted at. */
20
+ routes: ReadonlyArray<readonly [plugin: RoutePlugin, options: {
21
+ prefix: string;
22
+ }]>;
23
+ }
24
+ /**
25
+ * Registers the admin API: the scope accepts API tokens and needs a signed-in caller (`authenticate`), then every group
26
+ * gets its own scope with its part, its guard and its routes. A token is checked against the part of the group a route
27
+ * is in; a group with `noTokens` is for what a leaked token must never reach.
28
+ */
29
+ export declare function registerAdminApi(adminApp: FastifyInstance, options: {
30
+ authenticate: Guard;
31
+ groups: readonly AdminRouteGroup[];
32
+ }): Promise<void>;
33
+ export {};
34
+ //# sourceMappingURL=routeScope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeScope.d.ts","sourceRoot":"","sources":["../src/routeScope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,0HAA0H;AAC1H,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,EAAE,MAAM,UAAO,GAAG,IAAI,CAE3E;AAED,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AACnE,KAAK,KAAK,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAA;AAE1C,uGAAuG;AACvG,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uEAAuE;IACvE,KAAK,EAAE,KAAK,CAAA;IACZ,yEAAyE;IACzE,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACnF;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE;IAAE,YAAY,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;CAAE,GACnE,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Marks every route registered in `scope` as belonging to a part of the API, which is what an API token is checked
3
+ * against (`<part>:read` for GET, `<part>:write` otherwise). A route without a part is refused to every token.
4
+ */
5
+ export function belongsTo(scope, part) {
6
+ scope.addHook('onRoute', (route) => { route.config = { ...route.config, tokenScope: part }; });
7
+ }
8
+ /** Lets (or, with `false`, stops) the routes registered in `scope` accept an API token. Routes accept none by default. */
9
+ export function acceptApiTokens(scope, accept = true) {
10
+ scope.addHook('onRoute', (route) => { route.config = { ...route.config, allowApiToken: accept }; });
11
+ }
12
+ /**
13
+ * Registers the admin API: the scope accepts API tokens and needs a signed-in caller (`authenticate`), then every group
14
+ * gets its own scope with its part, its guard and its routes. A token is checked against the part of the group a route
15
+ * is in; a group with `noTokens` is for what a leaked token must never reach.
16
+ */
17
+ export async function registerAdminApi(adminApp, options) {
18
+ acceptApiTokens(adminApp);
19
+ adminApp.addHook('preHandler', options.authenticate);
20
+ for (const group of options.groups) {
21
+ await adminApp.register(async (scope) => {
22
+ if (group.noTokens)
23
+ acceptApiTokens(scope, false);
24
+ if (group.part !== undefined)
25
+ belongsTo(scope, group.part);
26
+ scope.addHook('preHandler', group.guard);
27
+ for (const [plugin, routeOptions] of group.routes)
28
+ await scope.register(plugin, routeOptions);
29
+ });
30
+ }
31
+ }
32
+ //# sourceMappingURL=routeScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeScope.js","sourceRoot":"","sources":["../src/routeScope.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAsB,EAAE,IAAY;IAC5D,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,GAAG,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;AAC/F,CAAC;AAED,0HAA0H;AAC1H,MAAM,UAAU,eAAe,CAAC,KAAsB,EAAE,MAAM,GAAG,IAAI;IACnE,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,GAAG,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;AACpG,CAAC;AAiBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,QAAyB,EACzB,OAAoE;IAEpE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,YAAqB,CAAC,CAAA;IAC7D,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YACtC,IAAI,KAAK,CAAC,QAAQ;gBAAE,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACjD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;gBAAE,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;YAC1D,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,KAAc,CAAC,CAAA;YACjD,KAAK,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,IAAI,KAAK,CAAC,MAAM;gBAAE,MAAM,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;QAC/F,CAAC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { AuthOptions } from './context.js';
3
+ import type { ApiTokens } from './apiTokens.js';
4
+ export type AuthMethod = 'password' | 'oidc';
5
+ export interface AuthIdentity {
6
+ userId: number;
7
+ email: string;
8
+ role: string;
9
+ sessionId: string;
10
+ mustChangePassword: boolean;
11
+ twoFactorEnabled: boolean;
12
+ authMethod: AuthMethod;
13
+ /**
14
+ * Set when the request is authenticated by an API token instead of a session. A token belongs to no user: `userId`
15
+ * is 0, `email` is `token: <name>` (it names the token in the audit log) and `sessionId` is a synthetic
16
+ * `api-token:<id>`. A token has no role: `role` is a placeholder
17
+ * that lets it through the role checks, and what it may do is decided by `scopes`, checked for every request in
18
+ * authenticateOrReject.
19
+ */
20
+ apiToken?: {
21
+ id: number;
22
+ name: string;
23
+ scopes: string[];
24
+ };
25
+ }
26
+ export interface AuthenticateOptions {
27
+ /** API tokens are only accepted on routes that opt in; everywhere else they are refused. */
28
+ allowApiToken?: boolean;
29
+ }
30
+ export declare const API_TOKEN_NOT_ACCEPTED = "API token not accepted here";
31
+ export declare function createSessions(ctx: AuthOptions, apiTokens: ApiTokens): {
32
+ createAuthSession: (app: FastifyInstance, reply: FastifyReply, user: {
33
+ id: number;
34
+ email: string;
35
+ role: string;
36
+ mustChangePassword: number;
37
+ totpEnabled: number;
38
+ }, authMethod?: AuthMethod) => Promise<AuthIdentity>;
39
+ clearAuthCookies: (reply: FastifyReply) => void;
40
+ authenticateRequest: (req: FastifyRequest, options?: AuthenticateOptions) => Promise<AuthIdentity>;
41
+ verifyCsrf: (req: FastifyRequest, identity: AuthIdentity) => Promise<void>;
42
+ revokeSession: (sessionId: string) => Promise<void>;
43
+ revokeUserSessions: (userId: number, exceptSessionId?: string) => Promise<void>;
44
+ };
45
+ export type Sessions = ReturnType<typeof createSessions>;
46
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAI/C,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,MAAM,CAAA;AAE5C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,UAAU,EAAE,UAAU,CAAA;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAC1D;AAED,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,eAAO,MAAM,sBAAsB,gCAAgC,CAAA;AASnE,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;6BA4B5D,eAAe,SACb,YAAY,QACb;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,eACtF,UAAU,KACrB,OAAO,CAAC,YAAY,CAAC;8BAwCS,YAAY,KAAG,IAAI;+BA6BZ,cAAc,YAAW,mBAAmB,KAAQ,OAAO,CAAC,YAAY,CAAC;sBAmClF,cAAc,YAAY,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC;+BAa7C,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;iCAKpB,MAAM,oBAAoB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;EAS3F;AAED,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA"}
@@ -0,0 +1,159 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { and, eq, lt, ne } from 'drizzle-orm';
3
+ import { authSessions, users } from './tables.js';
4
+ const SESSION_SECONDS = 12 * 60 * 60;
5
+ export const API_TOKEN_NOT_ACCEPTED = 'API token not accepted here';
6
+ export function createSessions(ctx, apiTokens) {
7
+ const { db, roles } = ctx;
8
+ const hooks = ctx.hooks ?? {};
9
+ const SESSION_COOKIE = ctx.cookies.session;
10
+ const CSRF_COOKIE = ctx.cookies.csrf;
11
+ const { findUsableApiToken, isApiToken, parseScopes } = apiTokens;
12
+ function cookieOptions(httpOnly) {
13
+ return {
14
+ path: '/',
15
+ httpOnly,
16
+ secure: ctx.secureCookies(),
17
+ sameSite: 'strict',
18
+ maxAge: SESSION_SECONDS,
19
+ };
20
+ }
21
+ function hash(value) {
22
+ return createHash('sha256').update(value).digest('hex');
23
+ }
24
+ function equalHash(left, right) {
25
+ const a = Buffer.from(left);
26
+ const b = Buffer.from(right);
27
+ return a.length === b.length && timingSafeEqual(a, b);
28
+ }
29
+ async function createAuthSession(app, reply, user, authMethod = 'password') {
30
+ const now = Date.now();
31
+ const sessionId = randomUUID();
32
+ const csrfToken = randomBytes(32).toString('base64url');
33
+ const expiresAt = now + SESSION_SECONDS * 1000;
34
+ await db.delete(authSessions).where(lt(authSessions.expiresAt, now));
35
+ await db.insert(authSessions).values({
36
+ id: sessionId,
37
+ userId: user.id,
38
+ csrfTokenHash: hash(csrfToken),
39
+ createdAt: now,
40
+ lastSeenAt: now,
41
+ expiresAt,
42
+ authMethod,
43
+ verifiedAt: now,
44
+ });
45
+ const role = roles.normalize(user.role);
46
+ const token = app.jwt.sign({ userId: user.id, email: user.email, role, sessionId });
47
+ reply.setCookie(SESSION_COOKIE, token, cookieOptions(true));
48
+ reply.setCookie(CSRF_COOKIE, csrfToken, cookieOptions(false));
49
+ return {
50
+ userId: user.id,
51
+ email: user.email,
52
+ role,
53
+ sessionId,
54
+ mustChangePassword: mustChangePassword(user, authMethod),
55
+ twoFactorEnabled: !!user.totpEnabled,
56
+ authMethod,
57
+ };
58
+ }
59
+ /**
60
+ * A password session must replace a pending temporary password, so the one the administrator handed out stops
61
+ * working. An SSO session never has to: the SSO sign-in makes the temporary password unusable instead.
62
+ */
63
+ function mustChangePassword(user, authMethod) {
64
+ return !!user.mustChangePassword && authMethod !== 'oidc';
65
+ }
66
+ function clearAuthCookies(reply) {
67
+ const base = { path: '/', secure: ctx.secureCookies(), sameSite: 'strict' };
68
+ reply.clearCookie(SESSION_COOKIE, { ...base, httpOnly: true });
69
+ reply.clearCookie(CSRF_COOKIE, { ...base, httpOnly: false });
70
+ }
71
+ function requestToken(req) {
72
+ const authorization = req.headers.authorization;
73
+ if (authorization?.startsWith('Bearer '))
74
+ return authorization.slice(7);
75
+ return req.cookies[SESSION_COOKIE] ?? null;
76
+ }
77
+ async function authenticateApiToken(req, token) {
78
+ const found = await findUsableApiToken(token);
79
+ if (!found)
80
+ throw new Error('Invalid API token');
81
+ const identity = {
82
+ userId: 0,
83
+ email: `token: ${found.name}`,
84
+ role: 'admin',
85
+ sessionId: `api-token:${found.id}`,
86
+ mustChangePassword: false,
87
+ twoFactorEnabled: false,
88
+ authMethod: 'password',
89
+ apiToken: { id: found.id, name: found.name, scopes: parseScopes(found.scopes) },
90
+ };
91
+ req.user = identity;
92
+ return identity;
93
+ }
94
+ async function authenticateRequest(req, options = {}) {
95
+ const token = requestToken(req);
96
+ if (!token)
97
+ throw new Error('Missing session');
98
+ if (isApiToken(token)) {
99
+ if (!options.allowApiToken)
100
+ throw new Error(API_TOKEN_NOT_ACCEPTED);
101
+ return authenticateApiToken(req, token);
102
+ }
103
+ const claims = req.server.jwt.verify(token);
104
+ if (!claims.sessionId || !Number.isInteger(claims.userId))
105
+ throw new Error('Invalid session');
106
+ const now = Date.now();
107
+ const session = (await db.select().from(authSessions).where(eq(authSessions.id, claims.sessionId)))[0];
108
+ if (!session || session.userId !== claims.userId || session.expiresAt <= now) {
109
+ if (session)
110
+ await db.delete(authSessions).where(eq(authSessions.id, session.id));
111
+ throw new Error('Expired session');
112
+ }
113
+ const user = (await db.select().from(users).where(eq(users.id, claims.userId)))[0];
114
+ if (!user)
115
+ throw new Error('Unknown user');
116
+ if (now - session.lastSeenAt >= 60_000) {
117
+ await db.update(authSessions).set({ lastSeenAt: now }).where(eq(authSessions.id, session.id));
118
+ }
119
+ const identity = {
120
+ userId: user.id,
121
+ email: user.email,
122
+ role: roles.normalize(user.role),
123
+ sessionId: session.id,
124
+ mustChangePassword: mustChangePassword(user, session.authMethod),
125
+ twoFactorEnabled: !!user.totpEnabled,
126
+ authMethod: session.authMethod === 'oidc' ? 'oidc' : 'password',
127
+ };
128
+ req.user = identity;
129
+ return identity;
130
+ }
131
+ async function verifyCsrf(req, identity) {
132
+ if (req.headers.authorization?.startsWith('Bearer '))
133
+ return;
134
+ if (['GET', 'HEAD', 'OPTIONS'].includes(req.method))
135
+ return;
136
+ const cookie = req.cookies[CSRF_COOKIE];
137
+ const header = req.headers['x-csrf-token'];
138
+ if (!cookie || typeof header !== 'string' || !equalHash(hash(cookie), hash(header))) {
139
+ throw new Error('Invalid CSRF token');
140
+ }
141
+ const session = (await db.select({ csrfTokenHash: authSessions.csrfTokenHash })
142
+ .from(authSessions).where(eq(authSessions.id, identity.sessionId)))[0];
143
+ if (!session || !equalHash(session.csrfTokenHash, hash(header)))
144
+ throw new Error('Invalid CSRF token');
145
+ }
146
+ async function revokeSession(sessionId) {
147
+ await db.delete(authSessions).where(eq(authSessions.id, sessionId));
148
+ hooks.onSessionsRevoked?.({ sessionId });
149
+ }
150
+ async function revokeUserSessions(userId, exceptSessionId) {
151
+ const condition = exceptSessionId
152
+ ? and(eq(authSessions.userId, userId), ne(authSessions.id, exceptSessionId))
153
+ : eq(authSessions.userId, userId);
154
+ await db.delete(authSessions).where(condition);
155
+ hooks.onSessionsRevoked?.({ userId, exceptSessionId });
156
+ }
157
+ return { createAuthSession, clearAuthCookies, authenticateRequest, verifyCsrf, revokeSession, revokeUserSessions };
158
+ }
159
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { FastifyInstance, FastifyReply } from 'fastify';
2
+ import type { SqliteDatabase } from '@better-forge/db';
3
+ import { users } from './tables.js';
4
+ export interface SetupRoutesOptions<Identity extends {
5
+ sessionId?: string;
6
+ }> {
7
+ db: SqliteDatabase;
8
+ /** Runs `work` so that all of it is written, or none of it. */
9
+ withTransaction: <T>(work: () => Promise<T>) => Promise<T>;
10
+ isSetupComplete: () => boolean;
11
+ /** Records that setup is done. Called as the last step inside the transaction: if it throws, the administrator is not kept. */
12
+ writeSetupComplete: () => void;
13
+ /** Takes the record back, when the transaction fails to commit after it was written. */
14
+ removeSetupMarker: () => void;
15
+ /** Opens the database and brings its tables up to date; called once the request has been checked. */
16
+ prepareDatabase: () => void;
17
+ /** Signs the new administrator in. */
18
+ createSession: (app: FastifyInstance, reply: FastifyReply, user: typeof users.$inferSelect) => Promise<Identity>;
19
+ /** Rate limit of `POST /complete` (the config the rate limit plugin understands). */
20
+ rateLimit: Record<string, unknown>;
21
+ /** Writes whatever else the application needs on first run, inside the same transaction as the administrator. */
22
+ onSetup?: () => Promise<void>;
23
+ /** Starts the work that runs only once the application is set up. */
24
+ startBackgroundServices?: () => void;
25
+ /** Lets setup run although accounts already exist, for an installation that is filled with accounts before its first run. Checked on each request. */
26
+ allowExistingUsers?: () => boolean;
27
+ }
28
+ /**
29
+ * The first-run routes: whether the application still needs setup, and the one request that creates the administrator
30
+ * and finishes it. Setup is refused once it is done, and only one attempt runs at a time.
31
+ */
32
+ export declare function createSetupRoutes<Identity extends {
33
+ sessionId?: string;
34
+ }>(options: SetupRoutesOptions<Identity>): (app: FastifyInstance, overrides?: {
35
+ startBackgroundServices?: () => void;
36
+ }) => Promise<void>;
37
+ //# sourceMappingURL=setupRoutes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setupRoutes.d.ts","sourceRoot":"","sources":["../src/setupRoutes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAG5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAEnC,MAAM,WAAW,kBAAkB,CAAC,QAAQ,SAAS;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;IACzE,EAAE,EAAE,cAAc,CAAA;IAClB,+DAA+D;IAC/D,eAAe,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC1D,eAAe,EAAE,MAAM,OAAO,CAAA;IAC9B,+HAA+H;IAC/H,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,wFAAwF;IACxF,iBAAiB,EAAE,MAAM,IAAI,CAAA;IAC7B,qGAAqG;IACrG,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,sCAAsC;IACtC,aAAa,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,KAAK,CAAC,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAChH,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,iHAAiH;IACjH,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,qEAAqE;IACrE,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAA;IACpC,sJAAsJ;IACtJ,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAA;CACnC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,SAAS;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,kBAAkB,CAAC,QAAQ,CAAC,IAI5E,KAAK,eAAe,EAAE,YAAW;IAAE,uBAAuB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,mBA8DjH"}