@afora/core 0.0.9 → 0.0.12
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/README.md +62 -189
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { recordPluginOf } from "../core/record/plugin.js";
|
|
2
|
+
import { recordPluginCatalogOf } from "../core/record/plugin-catalog.js";
|
|
3
|
+
import { recordOf } from "../core/record/record.js";
|
|
4
|
+
import { blueprintOf } from "./blueprint.js";
|
|
5
|
+
import { Password } from "./properties/password.js";
|
|
6
|
+
const now = () => new Date().toISOString();
|
|
7
|
+
|
|
8
|
+
export class PublishableRecord extends recordOf(blueprintOf((ctx) => ({ publishedAt: ctx.optional.dateTime }))) {
|
|
9
|
+
get isPublished() {
|
|
10
|
+
return this.publishedAt !== undefined;
|
|
11
|
+
}
|
|
12
|
+
publish(at = now()) {
|
|
13
|
+
return this.set("publishedAt", at);
|
|
14
|
+
}
|
|
15
|
+
unpublish() {
|
|
16
|
+
return this.set("publishedAt", undefined);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const Publishable = recordPluginOf("publishable", PublishableRecord, {
|
|
20
|
+
description: "A publication date, with publish, unpublish and isPublished."
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export class ArchivableRecord extends recordOf(blueprintOf((ctx) => ({ archivedAt: ctx.optional.dateTime }))) {
|
|
24
|
+
get isArchived() {
|
|
25
|
+
return this.archivedAt !== undefined;
|
|
26
|
+
}
|
|
27
|
+
archive(at = now()) {
|
|
28
|
+
return this.set("archivedAt", at);
|
|
29
|
+
}
|
|
30
|
+
restore() {
|
|
31
|
+
return this.set("archivedAt", undefined);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export const Archivable = recordPluginOf("archivable", ArchivableRecord, {
|
|
35
|
+
description: "An archive date, with archive, restore and isArchived."
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export class TaggableRecord extends recordOf(blueprintOf((ctx) => ({ tags: ctx.list.optional.string }))) {
|
|
39
|
+
hasTag(tag) {
|
|
40
|
+
return this.tags?.includes(tag) ?? false;
|
|
41
|
+
}
|
|
42
|
+
tag(...tags) {
|
|
43
|
+
return this.set("tags", [...new Set([...this.tags ?? [], ...tags])]);
|
|
44
|
+
}
|
|
45
|
+
untag(...tags) {
|
|
46
|
+
return this.set("tags", (this.tags ?? []).filter((tag) => !tags.includes(tag)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const Taggable = recordPluginOf("taggable", TaggableRecord, {
|
|
50
|
+
description: "A list of tags, with tag, untag and hasTag."
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export class OwnableRecord extends recordOf(blueprintOf((ctx) => ({ ownerId: ctx.id }))) {
|
|
54
|
+
isOwnedBy(id) {
|
|
55
|
+
return this.ownerId === id.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
transferTo(id) {
|
|
58
|
+
return this.set("ownerId", id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const Ownable = recordPluginOf("ownable", OwnableRecord, {
|
|
62
|
+
description: "The id of the owner, with isOwnedBy and transferTo."
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export class AuthenticatableRecord extends recordOf(blueprintOf((ctx) => ({ email: ctx.email, password: ctx.password })), { unique: ["email"], change: ["password"] }) {
|
|
66
|
+
verifyPassword(plain) {
|
|
67
|
+
return new Password(this.password).verify(plain);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export const Authenticatable = recordPluginOf("authenticatable", AuthenticatableRecord, {
|
|
71
|
+
description: "A unique email and a sensitive password hash, with change.password and verifyPassword."
|
|
72
|
+
});
|
|
73
|
+
export const StdRecordPlugins = recordPluginCatalogOf(Publishable, Archivable, Taggable, Ownable, Authenticatable);
|
package/dist/std/record.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A record of `blueprint`, e.g. `class User extends recordOf(UserBlueprint) {}`,
|
|
3
3
|
* whose `options.plugins` picks from the standard plugins, as
|
|
4
|
-
* `(ctx) => [ctx.publishable]`, or takes any plugin made by
|
|
4
|
+
* `(ctx) => [ctx.publishable]`, or takes any plugin made by {@link recordPluginOf}.
|
|
5
|
+
*
|
|
6
|
+
* @throws {@link ReservedRecordKey} when a key of the blueprint is a member of the record.
|
|
7
|
+
* @throws {@link UndeclaredRecordProperty} when `sensitive`, `unique` or `change` names a key the blueprint does not declare.
|
|
8
|
+
* @throws {@link InvalidRecordDeclaration} when `name` is not an identifier.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { blueprintOf, recordOf } from "@afora/core";
|
|
13
|
+
*
|
|
14
|
+
* class Account extends recordOf(
|
|
15
|
+
* blueprintOf((ctx) => ({ email: ctx.email, token: ctx.string })),
|
|
16
|
+
* { sensitive: ["token"], change: ["email"] },
|
|
17
|
+
* ) {}
|
|
18
|
+
*
|
|
19
|
+
* const account = new Account({ email: "Ana@Example.com", token: "secret" });
|
|
20
|
+
* account.email; // "ana@example.com"
|
|
21
|
+
* account.change.email("ann@example.com"); // same as set("email", ...)
|
|
22
|
+
* JSON.stringify(account); // the token is masked
|
|
23
|
+
* ```
|
|
5
24
|
*/
|
|
6
25
|
export declare const recordOf: import("../index.js").RecordBuilder<Omit<import("../core/record/types/plugin.interface.js").RecordPluginsByName<readonly [import("../index.js").IRecordPlugin<typeof import("./record-plugins.js").PublishableRecord, "publishable">, import("../index.js").IRecordPlugin<typeof import("./record-plugins.js").ArchivableRecord, "archivable">, import("../index.js").IRecordPlugin<typeof import("./record-plugins.js").TaggableRecord, "taggable">, import("../index.js").IRecordPlugin<typeof import("./record-plugins.js").OwnableRecord, "ownable">, import("../index.js").IRecordPlugin<typeof import("./record-plugins.js").AuthenticatableRecord, "authenticatable">]>, never> & Record<never, never>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const ACTIONS = [
|
|
2
|
+
"create",
|
|
3
|
+
"get",
|
|
4
|
+
"list",
|
|
5
|
+
"update",
|
|
6
|
+
"delete"
|
|
7
|
+
];
|
|
8
|
+
function routeOf(action, path) {
|
|
9
|
+
switch (action) {
|
|
10
|
+
case "create":
|
|
11
|
+
return { method: "post", path, status: 201 };
|
|
12
|
+
case "list":
|
|
13
|
+
return { method: "get", path };
|
|
14
|
+
case "get":
|
|
15
|
+
return { method: "get", path: `${path}/{id}` };
|
|
16
|
+
case "update":
|
|
17
|
+
return { method: "patch", path: `${path}/{id}` };
|
|
18
|
+
default:
|
|
19
|
+
return { method: "delete", path: `${path}/{id}` };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function resourceOf(repository, options) {
|
|
23
|
+
const actions = options.actions ?? ACTIONS;
|
|
24
|
+
const { access } = options;
|
|
25
|
+
const byAction = access !== undefined && Object.keys(access).some((key) => ACTIONS.includes(key));
|
|
26
|
+
const accessOf = (action) => byAction ? access[action] : access;
|
|
27
|
+
return (ctx) => {
|
|
28
|
+
const useCases = {};
|
|
29
|
+
for (const action of actions) {
|
|
30
|
+
const declared = accessOf(action);
|
|
31
|
+
const make = ctx[action];
|
|
32
|
+
useCases[action] = make(repository, {
|
|
33
|
+
route: routeOf(action, options.path),
|
|
34
|
+
...declared === undefined ? {} : { access: declared }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return useCases;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../core/exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../core/exception/exception.js";
|
|
3
|
+
import { markExported } from "../core/exception/exported.js";
|
|
4
|
+
export const PasswordHasherRequired = exceptionOf({
|
|
5
|
+
layer: ExceptionLayers.Property,
|
|
6
|
+
name: "PasswordHasherRequired",
|
|
7
|
+
message: "No password hasher is available: this runtime has neither Bun.password nor native argon2 (node:crypto, Node >= 24.7). Call configure({ hasher }) with your own PasswordHasher."
|
|
8
|
+
});
|
|
9
|
+
export const SyncVerifyUnsupported = exceptionOf({
|
|
10
|
+
layer: ExceptionLayers.Property,
|
|
11
|
+
name: "SyncVerifyUnsupported",
|
|
12
|
+
message: "The configured password hasher has no verifySync. Use verify, or configure a hasher that provides verifySync."
|
|
13
|
+
});
|
|
14
|
+
export const UnsupportedPasswordHash = exceptionOf({
|
|
15
|
+
layer: ExceptionLayers.Property,
|
|
16
|
+
name: "UnsupportedPasswordHash",
|
|
17
|
+
message: ({ version }) => `The password hash uses argon2 version ${version}, but native argon2 (node:crypto) only derives version ${ARGON2_VERSION}. Verify it with Bun.password or configure({ hasher }).`
|
|
18
|
+
});
|
|
19
|
+
markExported(PasswordHasherRequired, SyncVerifyUnsupported);
|
|
20
|
+
const ARGON2_VERSION = 19;
|
|
21
|
+
const ARGON2_PARAMS = {
|
|
22
|
+
memory: 65536,
|
|
23
|
+
passes: 2,
|
|
24
|
+
parallelism: 1,
|
|
25
|
+
tagLength: 32,
|
|
26
|
+
saltLength: 32,
|
|
27
|
+
version: ARGON2_VERSION
|
|
28
|
+
};
|
|
29
|
+
export const toBase64 = (bytes) => btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join("")).replace(/=+$/, "");
|
|
30
|
+
export const fromBase64 = (text) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
|
|
31
|
+
export function encodePhc(salt, tag) {
|
|
32
|
+
const { memory, passes, parallelism, version } = ARGON2_PARAMS;
|
|
33
|
+
return `$argon2id$v=${version}$m=${memory},t=${passes},p=${parallelism}$${toBase64(salt)}$${toBase64(tag)}`;
|
|
34
|
+
}
|
|
35
|
+
const PHC = /^\$argon2id\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/;
|
|
36
|
+
export function decodePhc(phc) {
|
|
37
|
+
const match = PHC.exec(phc);
|
|
38
|
+
if (!match)
|
|
39
|
+
return;
|
|
40
|
+
return {
|
|
41
|
+
version: Number(match[1]),
|
|
42
|
+
memory: Number(match[2]),
|
|
43
|
+
passes: Number(match[3]),
|
|
44
|
+
parallelism: Number(match[4]),
|
|
45
|
+
salt: fromBase64(match[5]),
|
|
46
|
+
tag: fromBase64(match[6])
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function nodeHasherOf(crypto) {
|
|
50
|
+
const { argon2, argon2Sync, randomBytes, timingSafeEqual } = crypto;
|
|
51
|
+
if (!argon2 || !argon2Sync || !randomBytes || !timingSafeEqual) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const derive = (parameters) => new Promise((resolve, reject) => {
|
|
55
|
+
argon2("argon2id", parameters, (error, key) => error ? reject(error) : resolve(key));
|
|
56
|
+
});
|
|
57
|
+
const parametersOf = (plain, parsed) => ({
|
|
58
|
+
message: plain,
|
|
59
|
+
nonce: parsed.salt,
|
|
60
|
+
parallelism: parsed.parallelism,
|
|
61
|
+
tagLength: parsed.tag.length,
|
|
62
|
+
memory: parsed.memory,
|
|
63
|
+
passes: parsed.passes
|
|
64
|
+
});
|
|
65
|
+
const decode = (hash) => {
|
|
66
|
+
const parsed = decodePhc(hash);
|
|
67
|
+
if (parsed && parsed.version !== ARGON2_VERSION) {
|
|
68
|
+
throw new UnsupportedPasswordHash({
|
|
69
|
+
source: "password",
|
|
70
|
+
details: { version: parsed.version }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
};
|
|
75
|
+
const matches = (tag, expected) => tag.length === expected.length && timingSafeEqual(tag, expected);
|
|
76
|
+
return {
|
|
77
|
+
async hash(plain) {
|
|
78
|
+
const salt = randomBytes(ARGON2_PARAMS.saltLength);
|
|
79
|
+
const tag = await derive({
|
|
80
|
+
message: plain,
|
|
81
|
+
nonce: salt,
|
|
82
|
+
parallelism: ARGON2_PARAMS.parallelism,
|
|
83
|
+
tagLength: ARGON2_PARAMS.tagLength,
|
|
84
|
+
memory: ARGON2_PARAMS.memory,
|
|
85
|
+
passes: ARGON2_PARAMS.passes
|
|
86
|
+
});
|
|
87
|
+
return encodePhc(salt, tag);
|
|
88
|
+
},
|
|
89
|
+
async verify(plain, hash) {
|
|
90
|
+
const parsed = decode(hash);
|
|
91
|
+
if (!parsed)
|
|
92
|
+
return false;
|
|
93
|
+
return matches(await derive(parametersOf(plain, parsed)), parsed.tag);
|
|
94
|
+
},
|
|
95
|
+
verifySync(plain, hash) {
|
|
96
|
+
const parsed = decode(hash);
|
|
97
|
+
if (!parsed)
|
|
98
|
+
return false;
|
|
99
|
+
const tag = argon2Sync("argon2id", parametersOf(plain, parsed));
|
|
100
|
+
return matches(tag, parsed.tag);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function bunHasherOf(password) {
|
|
105
|
+
return {
|
|
106
|
+
hash: (plain) => password.hash(plain, { algorithm: "argon2id" }),
|
|
107
|
+
verify: (plain, hash) => password.verify(plain, hash),
|
|
108
|
+
verifySync: (plain, hash) => password.verifySync(plain, hash)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function missingHasher() {
|
|
112
|
+
return {
|
|
113
|
+
hash: () => Promise.reject(new PasswordHasherRequired({ source: "password" })),
|
|
114
|
+
verify: () => Promise.reject(new PasswordHasherRequired({ source: "password" })),
|
|
115
|
+
verifySync: () => {
|
|
116
|
+
throw new PasswordHasherRequired({ source: "password" });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function defaultHasherOf(environment) {
|
|
121
|
+
if (environment.bun?.password) {
|
|
122
|
+
return bunHasherOf(environment.bun.password);
|
|
123
|
+
}
|
|
124
|
+
const node = environment.crypto && nodeHasherOf(environment.crypto);
|
|
125
|
+
return node ?? missingHasher();
|
|
126
|
+
}
|
|
127
|
+
let lastTimestamp = 0;
|
|
128
|
+
let sequence = 0;
|
|
129
|
+
const HEX = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, "0"));
|
|
130
|
+
const RANDOM_PER_ID = 10;
|
|
131
|
+
const randomPool = new Uint8Array(RANDOM_PER_ID * 64);
|
|
132
|
+
let randomAt = randomPool.length;
|
|
133
|
+
export function uuidv7(now = Date.now()) {
|
|
134
|
+
if (randomAt === randomPool.length) {
|
|
135
|
+
globalThis.crypto.getRandomValues(randomPool);
|
|
136
|
+
randomAt = 0;
|
|
137
|
+
}
|
|
138
|
+
const at = randomAt;
|
|
139
|
+
randomAt += RANDOM_PER_ID;
|
|
140
|
+
const random = randomPool;
|
|
141
|
+
const start = random[at + 8] << 8 | random[at + 9];
|
|
142
|
+
if (now > lastTimestamp) {
|
|
143
|
+
lastTimestamp = now;
|
|
144
|
+
sequence = start & 2047;
|
|
145
|
+
} else if (sequence < 4095) {
|
|
146
|
+
sequence++;
|
|
147
|
+
} else {
|
|
148
|
+
lastTimestamp++;
|
|
149
|
+
sequence = start & 2047;
|
|
150
|
+
}
|
|
151
|
+
const high = Math.floor(lastTimestamp / 2 ** 32);
|
|
152
|
+
const low = lastTimestamp % 2 ** 32;
|
|
153
|
+
const hex = (byte) => HEX[byte & 255];
|
|
154
|
+
return hex(high >>> 8) + hex(high) + hex(low >>> 24) + hex(low >>> 16) + "-" + hex(low >>> 8) + hex(low) + "-" + hex(112 | sequence >>> 8) + hex(sequence) + "-" + hex(random[at] & 63 | 128) + hex(random[at + 1]) + "-" + hex(random[at + 2]) + hex(random[at + 3]) + hex(random[at + 4]) + hex(random[at + 5]) + hex(random[at + 6]) + hex(random[at + 7]);
|
|
155
|
+
}
|
|
156
|
+
let hasher;
|
|
157
|
+
const defaultIdGenerator = () => uuidv7();
|
|
158
|
+
let idGenerator = defaultIdGenerator;
|
|
159
|
+
export function configure(services) {
|
|
160
|
+
if (services.hasher)
|
|
161
|
+
hasher = services.hasher;
|
|
162
|
+
if (services.idGenerator)
|
|
163
|
+
idGenerator = services.idGenerator;
|
|
164
|
+
}
|
|
165
|
+
export function resetRuntime() {
|
|
166
|
+
hasher = undefined;
|
|
167
|
+
idGenerator = defaultIdGenerator;
|
|
168
|
+
lastTimestamp = 0;
|
|
169
|
+
sequence = 0;
|
|
170
|
+
}
|
|
171
|
+
function nodeCryptoOf() {
|
|
172
|
+
const process = globalThis.process;
|
|
173
|
+
return process?.getBuiltinModule?.("node:crypto");
|
|
174
|
+
}
|
|
175
|
+
export function currentHasher() {
|
|
176
|
+
hasher ??= defaultHasherOf({
|
|
177
|
+
bun: globalThis.Bun,
|
|
178
|
+
crypto: nodeCryptoOf()
|
|
179
|
+
});
|
|
180
|
+
return hasher;
|
|
181
|
+
}
|
|
182
|
+
export function makesDefaultIds() {
|
|
183
|
+
return idGenerator === defaultIdGenerator;
|
|
184
|
+
}
|
|
185
|
+
export function generateId() {
|
|
186
|
+
return idGenerator();
|
|
187
|
+
}
|
|
188
|
+
export function verifySyncWithCurrent(plain, hash) {
|
|
189
|
+
const current = currentHasher();
|
|
190
|
+
if (!current.verifySync)
|
|
191
|
+
throw new SyncVerifyUnsupported({ source: "password" });
|
|
192
|
+
return current.verifySync(plain, hash);
|
|
193
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jwtOf, jwtOptionsIssue } from "../../core/auth/jwt.js";
|
|
2
|
+
import { outputValueOf } from "../../core/output/problem.js";
|
|
3
|
+
import {
|
|
4
|
+
UseCaseEntityConflict,
|
|
5
|
+
UseCaseInvalidCredentials
|
|
6
|
+
} from "../../core/use-case/exceptions.js";
|
|
7
|
+
import { useCasePluginOf } from "../../core/use-case/plugin.js";
|
|
8
|
+
import { StringProperty } from "../properties/primitives.js";
|
|
9
|
+
import { createCompilation } from "./crud.js";
|
|
10
|
+
import {
|
|
11
|
+
asOf,
|
|
12
|
+
checkOptions,
|
|
13
|
+
errorOf,
|
|
14
|
+
fields,
|
|
15
|
+
metaOf
|
|
16
|
+
} from "./shared.js";
|
|
17
|
+
const TOKEN_KEYS = ["refresh", "scopes", "scopesFrom", "claims"];
|
|
18
|
+
const isTexts = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
19
|
+
function tokensOf(target, options) {
|
|
20
|
+
const given = options.token;
|
|
21
|
+
if (given === undefined)
|
|
22
|
+
return;
|
|
23
|
+
const bad = jwtOptionsIssue(given, "the option token", TOKEN_KEYS, true);
|
|
24
|
+
if (bad)
|
|
25
|
+
return target.unavailable(bad);
|
|
26
|
+
const { refresh, scopes, scopesFrom, claims, ...jwtOptions } = given;
|
|
27
|
+
if (refresh !== undefined && (typeof refresh !== "object" || refresh === null || Object.keys(refresh).join() !== "expiresIn" || !Number.isInteger(refresh.expiresIn) || refresh.expiresIn <= 0)) {
|
|
28
|
+
return target.unavailable("the option token.refresh must be { expiresIn }, a whole number of seconds above 0");
|
|
29
|
+
}
|
|
30
|
+
if (scopes !== undefined && !isTexts(scopes)) {
|
|
31
|
+
return target.unavailable("the option token.scopes must be a list of scopes");
|
|
32
|
+
}
|
|
33
|
+
if (scopesFrom !== undefined)
|
|
34
|
+
target.field(scopesFrom);
|
|
35
|
+
if (claims !== undefined && (typeof claims !== "object" || claims === null || Array.isArray(claims))) {
|
|
36
|
+
return target.unavailable("the option token.claims must map claims to keys of the entity");
|
|
37
|
+
}
|
|
38
|
+
const claimKeys = Object.entries(claims ?? {});
|
|
39
|
+
for (const [, key] of claimKeys)
|
|
40
|
+
target.field(key);
|
|
41
|
+
const jwt = jwtOf(jwtOptions);
|
|
42
|
+
const issue = async (entity) => {
|
|
43
|
+
const held = scopesFrom === undefined ? [] : entity[scopesFrom];
|
|
44
|
+
const subject = {
|
|
45
|
+
subject: String(entity.id),
|
|
46
|
+
scopes: [...new Set([...scopes ?? [], ...isTexts(held) ? held : []])],
|
|
47
|
+
...claimKeys.length > 0 ? {
|
|
48
|
+
claims: Object.fromEntries(claimKeys.map(([claim, key]) => [
|
|
49
|
+
claim,
|
|
50
|
+
outputValueOf(entity[key])
|
|
51
|
+
]))
|
|
52
|
+
} : {}
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
token: await jwt.sign(subject),
|
|
56
|
+
...refresh ? {
|
|
57
|
+
refreshToken: await jwt.sign({ subject: subject.subject }, { expiresIn: refresh.expiresIn, use: "refresh" })
|
|
58
|
+
} : {}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const verifyRefresh = (token) => jwt.verify(token, { use: "refresh" });
|
|
62
|
+
return { issue, verifyRefresh, refresh: refresh !== undefined };
|
|
63
|
+
}
|
|
64
|
+
const authenticatable = (actions) => ({
|
|
65
|
+
requires: { actions, plugins: ["authenticatable"] }
|
|
66
|
+
});
|
|
67
|
+
export const register = useCasePluginOf("register", {
|
|
68
|
+
description: "Stores an authenticatable entity built from the input, its password given plain and stored as a hash, unless the email is taken.",
|
|
69
|
+
...authenticatable(["findByEmail", "create"]),
|
|
70
|
+
compile: (target, options) => {
|
|
71
|
+
const conflict = errorOf(target, options, "conflict", UseCaseEntityConflict);
|
|
72
|
+
return createCompilation(target, options, "Register", (input) => [
|
|
73
|
+
{
|
|
74
|
+
find: "repository.findByEmail",
|
|
75
|
+
args: [`${input}.email`],
|
|
76
|
+
as: "existing"
|
|
77
|
+
},
|
|
78
|
+
{ ensure: "absent", value: "$existing", error: conflict }
|
|
79
|
+
], [conflict]);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
export const login = useCasePluginOf("login", {
|
|
83
|
+
description: "Gives the authenticatable entity of an email when the password matches, with a token (and a refresh token) for it when the option token is given.",
|
|
84
|
+
...authenticatable(["findByEmail"]),
|
|
85
|
+
compile: (target, options) => {
|
|
86
|
+
checkOptions(target, options, ["as", "token"]);
|
|
87
|
+
const tokens = tokensOf(target, options);
|
|
88
|
+
const invalid = errorOf(target, options, "invalid", UseCaseInvalidCredentials);
|
|
89
|
+
return {
|
|
90
|
+
...metaOf(target, options, "Login"),
|
|
91
|
+
blueprint: fields(() => ({
|
|
92
|
+
email: target.field("email").field,
|
|
93
|
+
password: StringProperty
|
|
94
|
+
})),
|
|
95
|
+
response: fields(() => ({
|
|
96
|
+
[asOf(target, options)]: target.entity,
|
|
97
|
+
...tokens ? { token: StringProperty } : {},
|
|
98
|
+
...tokens?.refresh ? { refreshToken: StringProperty } : {}
|
|
99
|
+
})),
|
|
100
|
+
dependencies: { repository: target.repository },
|
|
101
|
+
errors: [invalid],
|
|
102
|
+
steps: [
|
|
103
|
+
{ find: "repository.findByEmail", args: ["$input.email"], as: "item" },
|
|
104
|
+
{ ensure: "exists", value: "$item", error: invalid },
|
|
105
|
+
{
|
|
106
|
+
invoke: "$item",
|
|
107
|
+
method: "verifyPassword",
|
|
108
|
+
args: ["$input.password"],
|
|
109
|
+
as: "matches"
|
|
110
|
+
},
|
|
111
|
+
{ ensure: "true", value: "$matches", error: invalid },
|
|
112
|
+
...tokens ? [
|
|
113
|
+
{
|
|
114
|
+
run: "issueTokens",
|
|
115
|
+
fn: tokens.issue,
|
|
116
|
+
args: ["$item"],
|
|
117
|
+
as: "tokens"
|
|
118
|
+
}
|
|
119
|
+
] : [],
|
|
120
|
+
{
|
|
121
|
+
return: {
|
|
122
|
+
[asOf(target, options)]: "$item",
|
|
123
|
+
...tokens ? { token: "$tokens.token" } : {},
|
|
124
|
+
...tokens?.refresh ? { refreshToken: "$tokens.refreshToken" } : {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
export const refresh = useCasePluginOf("refresh", {
|
|
132
|
+
description: "Exchanges a refresh token issued by login for a new token and a new refresh token, for the entity it names, if it still exists. Experimental.",
|
|
133
|
+
requires: { actions: ["findById"] },
|
|
134
|
+
compile: (target, options) => {
|
|
135
|
+
checkOptions(target, options, ["token"]);
|
|
136
|
+
const tokens = tokensOf(target, options);
|
|
137
|
+
if (!tokens?.refresh) {
|
|
138
|
+
return target.unavailable("it needs the option token with refresh, the same login was given");
|
|
139
|
+
}
|
|
140
|
+
const invalid = errorOf(target, options, "invalid", UseCaseInvalidCredentials);
|
|
141
|
+
return {
|
|
142
|
+
...metaOf(target, options, "Refresh"),
|
|
143
|
+
blueprint: fields(() => ({ refreshToken: StringProperty })),
|
|
144
|
+
response: fields(() => ({
|
|
145
|
+
token: StringProperty,
|
|
146
|
+
refreshToken: StringProperty
|
|
147
|
+
})),
|
|
148
|
+
dependencies: { repository: target.repository },
|
|
149
|
+
errors: [invalid],
|
|
150
|
+
steps: [
|
|
151
|
+
{
|
|
152
|
+
run: "verifyRefreshToken",
|
|
153
|
+
fn: tokens.verifyRefresh,
|
|
154
|
+
args: ["$input.refreshToken"],
|
|
155
|
+
as: "caller"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
find: "repository.findById",
|
|
159
|
+
args: ["$caller.subject"],
|
|
160
|
+
as: "item"
|
|
161
|
+
},
|
|
162
|
+
{ ensure: "exists", value: "$item", error: invalid },
|
|
163
|
+
{
|
|
164
|
+
run: "issueTokens",
|
|
165
|
+
fn: tokens.issue,
|
|
166
|
+
args: ["$item"],
|
|
167
|
+
as: "tokens"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
return: {
|
|
171
|
+
token: "$tokens.token",
|
|
172
|
+
refreshToken: "$tokens.refreshToken"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
export const logout = useCasePluginOf("logout", {
|
|
180
|
+
description: "Gives an empty token and refresh token: a route whose setCookie sends them removes the cookies. Tokens are not stored, so one already issued stays valid until it expires. Experimental.",
|
|
181
|
+
compile: (target, options) => {
|
|
182
|
+
checkOptions(target, options, []);
|
|
183
|
+
return {
|
|
184
|
+
...metaOf(target, options, "Logout"),
|
|
185
|
+
blueprint: fields(() => ({})),
|
|
186
|
+
response: fields(() => ({
|
|
187
|
+
token: StringProperty,
|
|
188
|
+
refreshToken: StringProperty
|
|
189
|
+
})),
|
|
190
|
+
steps: [{ return: { token: "", refreshToken: "" } }]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|