@afora/core 0.0.9 → 0.0.11

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 (186) hide show
  1. package/README.md +52 -30
  2. package/dist/adapters.js +31 -55
  3. package/dist/core/application/application.d.ts +30 -0
  4. package/dist/core/application/application.js +1291 -0
  5. package/dist/core/application/exceptions.js +85 -0
  6. package/dist/core/application/migrate.js +67 -0
  7. package/dist/core/application/providers.js +108 -0
  8. package/dist/core/application/registry.js +23 -0
  9. package/dist/core/application/schema.js +398 -0
  10. package/dist/core/application/start.js +162 -0
  11. package/dist/core/application/types/application.interface.d.ts +23 -3
  12. package/dist/core/auth/jwt.js +200 -0
  13. package/dist/core/auth/types/jwt.interface.d.ts +10 -2
  14. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  15. package/dist/core/blueprint/blueprint.js +146 -0
  16. package/dist/core/blueprint/callable.js +13 -0
  17. package/dist/core/blueprint/exceptions.js +20 -0
  18. package/dist/core/blueprint/is-blueprint.js +4 -0
  19. package/dist/core/blueprint/list.js +19 -0
  20. package/dist/core/blueprint/optional.js +69 -0
  21. package/dist/core/blueprint/record-field.js +14 -0
  22. package/dist/core/blueprint/slice.js +127 -0
  23. package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
  24. package/dist/core/blueprint/variant.js +72 -0
  25. package/dist/core/command/command.js +428 -0
  26. package/dist/core/command/exceptions.js +30 -0
  27. package/dist/core/command/types/command.interface.d.ts +10 -1
  28. package/dist/core/entity/abstract-entity.d.ts +19 -0
  29. package/dist/core/entity/abstract-entity.js +181 -0
  30. package/dist/core/entity/entity.js +209 -0
  31. package/dist/core/entity/event.d.ts +39 -0
  32. package/dist/core/entity/event.js +80 -0
  33. package/dist/core/entity/exceptions.js +27 -0
  34. package/dist/core/entity/trust.js +7 -0
  35. package/dist/core/entity/version.js +13 -0
  36. package/dist/core/environment/environment.d.ts +26 -3
  37. package/dist/core/environment/environment.js +71 -0
  38. package/dist/core/event-policy/event-policy.js +76 -0
  39. package/dist/core/event-policy/exceptions.js +12 -0
  40. package/dist/core/event-policy/publish.js +98 -0
  41. package/dist/core/event-policy/publishers.js +18 -0
  42. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  43. package/dist/core/exception/enums/exception-layers.js +15 -0
  44. package/dist/core/exception/exception.d.ts +15 -0
  45. package/dist/core/exception/exception.js +128 -0
  46. package/dist/core/exception/exported.js +49 -0
  47. package/dist/core/exception/guard.d.ts +20 -1
  48. package/dist/core/exception/guard.js +16 -0
  49. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  50. package/dist/core/json.js +19 -0
  51. package/dist/core/logger/exceptions.js +7 -0
  52. package/dist/core/logger/logger.d.ts +14 -1
  53. package/dist/core/logger/logger.js +178 -0
  54. package/dist/core/openapi/exceptions.js +7 -0
  55. package/dist/core/openapi/openapi.js +132 -0
  56. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  57. package/dist/core/output/access.js +66 -0
  58. package/dist/core/output/coerce.js +38 -0
  59. package/dist/core/output/exceptions.js +22 -0
  60. package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
  61. package/dist/core/output/openapi-sink.d.ts +10 -2
  62. package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
  63. package/dist/core/output/output.js +192 -0
  64. package/dist/core/output/problem.js +30 -0
  65. package/dist/core/output/select.js +28 -0
  66. package/dist/core/output/stage.js +111 -0
  67. package/dist/core/output/status.js +52 -0
  68. package/dist/core/output/types/http.interface.d.ts +5 -1
  69. package/dist/core/output/types/output.interface.d.ts +24 -0
  70. package/dist/core/parse/issue.js +31 -0
  71. package/dist/core/parse/parse.d.ts +8 -1
  72. package/dist/core/parse/parse.js +323 -0
  73. package/dist/core/parse/strip.js +48 -0
  74. package/dist/core/property/abstract-property.js +37 -0
  75. package/dist/core/property/define-property.js +209 -0
  76. package/dist/core/property/enums/property-states.js +8 -0
  77. package/dist/core/property/exceptions.js +12 -0
  78. package/dist/core/property/input.js +10 -0
  79. package/dist/core/property/is-property.js +3 -0
  80. package/dist/core/property/lineage.js +8 -0
  81. package/dist/core/property/property.d.ts +27 -0
  82. package/dist/core/property/property.js +46 -0
  83. package/dist/core/property/refine.js +103 -0
  84. package/dist/core/record/abstract-record.d.ts +2 -1
  85. package/dist/core/record/abstract-record.js +307 -0
  86. package/dist/core/record/exceptions.js +49 -0
  87. package/dist/core/record/marks.js +7 -0
  88. package/dist/core/record/mask.js +60 -0
  89. package/dist/core/record/name.js +23 -0
  90. package/dist/core/record/plugin-catalog.js +115 -0
  91. package/dist/core/record/plugin.js +198 -0
  92. package/dist/core/record/record.js +117 -0
  93. package/dist/core/record/types/record.interface.d.ts +5 -0
  94. package/dist/core/repository/cursor.js +83 -0
  95. package/dist/core/repository/exceptions.js +72 -0
  96. package/dist/core/repository/in-memory.js +196 -0
  97. package/dist/core/repository/migrate.js +254 -0
  98. package/dist/core/repository/plan.js +27 -0
  99. package/dist/core/repository/provider.js +29 -0
  100. package/dist/core/repository/query.js +153 -0
  101. package/dist/core/repository/references.js +196 -0
  102. package/dist/core/repository/repository.d.ts +22 -2
  103. package/dist/core/repository/repository.js +720 -0
  104. package/dist/core/repository/state.js +138 -0
  105. package/dist/core/repository/types/query.interface.d.ts +13 -0
  106. package/dist/core/repository/types/repository.interface.d.ts +20 -8
  107. package/dist/core/repository/types/schema.interface.d.ts +8 -2
  108. package/dist/core/repository/upcast.js +179 -0
  109. package/dist/core/schema/schema.d.ts +9 -0
  110. package/dist/core/schema/schema.js +542 -0
  111. package/dist/core/standard/standard-schema.js +1 -0
  112. package/dist/core/standard/standard.js +23 -0
  113. package/dist/core/symbols/demo.d.ts +4 -0
  114. package/dist/core/symbols/demo.js +1 -0
  115. package/dist/core/symbols/kind.js +1 -0
  116. package/dist/core/symbols/meta.js +1 -0
  117. package/dist/core/symbols/state.d.ts +5 -0
  118. package/dist/core/symbols/state.js +1 -0
  119. package/dist/core/tracing/exceptions.d.ts +4 -0
  120. package/dist/core/tracing/exceptions.js +7 -0
  121. package/dist/core/tracing/tracing.d.ts +72 -0
  122. package/dist/core/tracing/tracing.js +101 -0
  123. package/dist/core/use-case/access.js +56 -0
  124. package/dist/core/use-case/decorators.js +65 -0
  125. package/dist/core/use-case/exceptions.d.ts +5 -1
  126. package/dist/core/use-case/exceptions.js +48 -0
  127. package/dist/core/use-case/plugin-catalog.js +68 -0
  128. package/dist/core/use-case/plugin.js +133 -0
  129. package/dist/core/use-case/route.js +200 -0
  130. package/dist/core/use-case/scope.d.ts +5 -0
  131. package/dist/core/use-case/scope.js +4 -0
  132. package/dist/core/use-case/steps/describe.d.ts +5 -1
  133. package/dist/core/use-case/steps/describe.js +24 -0
  134. package/dist/core/use-case/steps/references.js +105 -0
  135. package/dist/core/use-case/steps/run.js +139 -0
  136. package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
  137. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  138. package/dist/core/use-case/steps/validate.js +368 -0
  139. package/dist/core/use-case/target.js +100 -0
  140. package/dist/core/use-case/types/route.interface.d.ts +25 -5
  141. package/dist/core/use-case/use-case.d.ts +77 -2
  142. package/dist/core/use-case/use-case.js +493 -0
  143. package/dist/experimental.d.ts +5 -0
  144. package/dist/experimental.js +18 -161
  145. package/dist/index.d.ts +7 -5
  146. package/dist/index.js +213 -367
  147. package/dist/std/blueprint-plugin.js +42 -0
  148. package/dist/std/blueprint.d.ts +21 -0
  149. package/dist/std/blueprint.js +3 -0
  150. package/dist/std/command.js +3 -0
  151. package/dist/std/entity.d.ts +20 -1
  152. package/dist/std/entity.js +30 -0
  153. package/dist/std/event-policies.js +3 -0
  154. package/dist/std/properties/contact.d.ts +3 -0
  155. package/dist/std/properties/contact.js +29 -0
  156. package/dist/std/properties/enum.js +26 -0
  157. package/dist/std/properties/exceptions.js +14 -0
  158. package/dist/std/properties/headless.js +45 -0
  159. package/dist/std/properties/identity.js +28 -0
  160. package/dist/std/properties/json.js +161 -0
  161. package/dist/std/properties/password.js +79 -0
  162. package/dist/std/properties/primitives.d.ts +4 -0
  163. package/dist/std/properties/primitives.js +28 -0
  164. package/dist/std/properties/time.js +55 -0
  165. package/dist/std/record-plugins.js +73 -0
  166. package/dist/std/record.d.ts +20 -1
  167. package/dist/std/record.js +3 -0
  168. package/dist/std/resource.js +39 -0
  169. package/dist/std/runtime.js +193 -0
  170. package/dist/std/stages.js +4 -0
  171. package/dist/std/use-cases/auth.js +193 -0
  172. package/dist/std/use-cases/crud.js +261 -0
  173. package/dist/std/use-cases/index.js +12 -0
  174. package/dist/std/use-cases/lifecycle.js +97 -0
  175. package/dist/std/use-cases/shared.js +232 -0
  176. package/dist/std/use-cases/types.d.ts +5 -1
  177. package/dist/std/use-cases/types.js +0 -0
  178. package/dist/testing/http-provider-contract.js +344 -0
  179. package/dist/testing/index.js +3 -1435
  180. package/dist/testing/memory-publisher.js +12 -0
  181. package/dist/testing/provider-contract.d.ts +12 -3
  182. package/dist/testing/provider-contract.js +1071 -0
  183. package/package.json +10 -3
  184. package/dist/index-3aae5jfq.js +0 -2997
  185. package/dist/index-ny3as79r.js +0 -4567
  186. package/dist/index-xegv6a1p.js +0 -4291
@@ -0,0 +1,162 @@
1
+ import { InvalidCommandDeclaration } from "../command/exceptions.js";
2
+ import {
3
+ isEnvironment,
4
+ loadEnvironments
5
+ } from "../environment/environment.js";
6
+ import { Kind } from "../symbols/kind.js";
7
+ import {
8
+ ApplicationNotStarted,
9
+ InvalidApplicationDeclaration
10
+ } from "./exceptions.js";
11
+ export function providerFrom(environmentDependencies, create) {
12
+ if (typeof environmentDependencies === "function" && create === undefined) {
13
+ return Object.freeze({
14
+ [Kind]: "provider-factory",
15
+ environments: Object.freeze([]),
16
+ create: environmentDependencies,
17
+ withContext: true
18
+ });
19
+ }
20
+ const list = isEnvironment(environmentDependencies) ? [environmentDependencies] : Array.isArray(environmentDependencies) ? [...environmentDependencies] : [null];
21
+ if (!list.every(isEnvironment) || new Set(list).size !== list.length || typeof create !== "function") {
22
+ throw new InvalidApplicationDeclaration({
23
+ source: "providerFrom",
24
+ details: {
25
+ reason: "Expected distinct environments made by environmentOf and a create function."
26
+ }
27
+ });
28
+ }
29
+ return Object.freeze({
30
+ [Kind]: "provider-factory",
31
+ environments: Object.freeze(list),
32
+ create,
33
+ withContext: false
34
+ });
35
+ }
36
+ export function logFrom(create) {
37
+ if (typeof create !== "function") {
38
+ throw new InvalidApplicationDeclaration({
39
+ source: "logFrom",
40
+ details: { reason: "Expected logFrom to take a create function." }
41
+ });
42
+ }
43
+ return Object.freeze({ [Kind]: "log-factory", create });
44
+ }
45
+ export function isLogFactory(value) {
46
+ return typeof value === "object" && value !== null && value[Kind] === "log-factory";
47
+ }
48
+ export function isProviderFactory(value) {
49
+ return typeof value === "object" && value !== null && value[Kind] === "provider-factory";
50
+ }
51
+ const deferred = new WeakMap;
52
+ const METHODS = [
53
+ "findOne",
54
+ "findMany",
55
+ "insert",
56
+ "update",
57
+ "remove",
58
+ "count"
59
+ ];
60
+ export function deferredProvider(factory, application) {
61
+ const state = {
62
+ factory,
63
+ application,
64
+ provider: undefined,
65
+ starting: undefined
66
+ };
67
+ const real = (method) => {
68
+ const { provider } = state;
69
+ if (!provider) {
70
+ throw new ApplicationNotStarted({
71
+ source: application,
72
+ details: { application }
73
+ });
74
+ }
75
+ if (typeof provider[method] !== "function") {
76
+ throw new InvalidApplicationDeclaration({
77
+ source: application,
78
+ details: {
79
+ reason: `The provider made on start has no "${method}".`
80
+ }
81
+ });
82
+ }
83
+ return provider;
84
+ };
85
+ const proxy = {};
86
+ for (const method of [...METHODS, "increment"]) {
87
+ proxy[method] = (...args) => real(method)[method]?.(...args);
88
+ }
89
+ proxy.transaction = (run) => real("transaction").transaction(run);
90
+ proxy.migrate = async (...args) => {
91
+ const { provider } = state;
92
+ if (!provider)
93
+ real("migrate");
94
+ return provider?.migrate?.(...args);
95
+ };
96
+ Object.defineProperty(proxy, "capabilities", {
97
+ get: () => state.provider?.capabilities,
98
+ enumerable: true
99
+ });
100
+ Object.freeze(proxy);
101
+ deferred.set(proxy, state);
102
+ return proxy;
103
+ }
104
+ export function isDeferredProvider(provider) {
105
+ return typeof provider === "object" && provider !== null && deferred.has(provider);
106
+ }
107
+ export function deferredEnvironments(provider) {
108
+ return (typeof provider === "object" && provider !== null ? deferred.get(provider)?.factory.environments : undefined) ?? [];
109
+ }
110
+ export function startDeferred(provider, context) {
111
+ const state = deferred.get(provider);
112
+ if (!state)
113
+ return;
114
+ state.starting ??= (async () => {
115
+ const made = await state.factory.create(state.factory.withContext ? context : loadEnvironments(state.factory.environments));
116
+ if (typeof made !== "object" || made === null) {
117
+ state.starting = undefined;
118
+ throw new InvalidApplicationDeclaration({
119
+ source: state.application,
120
+ details: {
121
+ reason: "Expected providerFrom's create to give a repository provider."
122
+ }
123
+ });
124
+ }
125
+ state.provider = made;
126
+ return made;
127
+ })();
128
+ return state.starting;
129
+ }
130
+ export async function stopProvider(provider) {
131
+ const state = deferred.get(provider);
132
+ if (!state) {
133
+ await provider.close?.();
134
+ return;
135
+ }
136
+ const made = state.provider;
137
+ state.provider = undefined;
138
+ state.starting = undefined;
139
+ await made?.close?.();
140
+ }
141
+ export function checkTransactions(source, provider, commands) {
142
+ if (commands.length === 0)
143
+ return;
144
+ if (typeof provider.transaction !== "function" || provider.capabilities?.transactions === false) {
145
+ throw new InvalidCommandDeclaration({
146
+ source,
147
+ details: {
148
+ reason: `The provider made on start does not support transactions, which ${commands.join(", ")} declare.`
149
+ }
150
+ });
151
+ }
152
+ }
153
+ export function checkCapabilities(source, provider, repositories) {
154
+ if (repositories.length === 0 || provider.capabilities?.keyset === true)
155
+ return;
156
+ throw new InvalidApplicationDeclaration({
157
+ source,
158
+ details: {
159
+ reason: `The provider made on start does not read by cursor (capabilities.keyset), which the findCursor of ${repositories.join(", ")} needs.`
160
+ }
161
+ });
162
+ }
@@ -11,6 +11,7 @@ import type { ProviderDescription, RepositoryIndex, RepositoryScans } from "../.
11
11
  import type { AnyRepository, IRepositoryProvider } from "../../repository/types/repository.interface.js";
12
12
  import type { SchemaLock, SchemaMigrationOptions, SchemaMigrationReport } from "../../repository/types/schema.interface.js";
13
13
  import type { Kind } from "../../symbols/kind.js";
14
+ import type { ITracer } from "../../tracing/tracing.js";
14
15
  import type { UseCaseStepDescription } from "../../use-case/steps/types/steps.interface.js";
15
16
  import type { AccessDeclaration } from "../../use-case/types/access.interface.js";
16
17
  import type { RouteDeclaration } from "../../use-case/types/route.interface.js";
@@ -101,6 +102,14 @@ export interface ApplicationSettings<Name extends string, Extended extends reado
101
102
  * it, the one of the last extended application that has one; none by default.
102
103
  */
103
104
  readonly log?: LogSettings | LogFactory;
105
+ /**
106
+ * What traces it: a span per command operation, use case and repository
107
+ * call made through a use case, nested as the calls are, with names and
108
+ * outcomes and never a value (see `docs/tracing.md`). `otelTracer()` of
109
+ * `@afora/otel` gives one for OpenTelemetry. Without it, the one of the
110
+ * last extended application that has one; none by default.
111
+ */
112
+ readonly trace?: ITracer;
104
113
  /**
105
114
  * Where event policies such as `publishEvents` publish, by key: a
106
115
  * WebSocket hub, a queue, a broker. Commands of the application find them
@@ -239,11 +248,17 @@ export interface IApplication<Name extends string, Collections extends Applicati
239
248
  export interface ApplicationStartOptions {
240
249
  /**
241
250
  * Become the started application of the process (what `useApplication()`
242
- * gives). Default `true`; another one already started throws
243
- * `ApplicationAlreadyStarted`.
251
+ * gives). Another one already started throws `ApplicationAlreadyStarted`.
252
+ *
253
+ * @default true
244
254
  */
245
255
  readonly register?: boolean;
246
- /** Stop the started application of the process and take its place, e.g. on hot reload. Default `false`. */
256
+ /**
257
+ * Stop the started application of the process and take its place, e.g. on hot
258
+ * reload.
259
+ *
260
+ * @default false
261
+ */
247
262
  readonly replace?: boolean;
248
263
  /**
249
264
  * The committed schema lock (`afora.lock.json`): start throws
@@ -318,6 +333,7 @@ export type FieldDescription = {
318
333
  readonly optional: boolean;
319
334
  readonly variants: Readonly<Record<string, FieldDescription>>;
320
335
  };
336
+ /** The fields of a blueprint, record or entity in a description, by name. */
321
337
  export type FieldsDescription = Readonly<Record<string, FieldDescription>>;
322
338
  /** A record or an entity: its fields (an entity's identity and its plugins' keys left out), options and plugins. */
323
339
  export interface RecordDescription {
@@ -485,6 +501,10 @@ export interface ApplicationDescription {
485
501
  readonly log?: LogDescription | {
486
502
  readonly kind: "opaque";
487
503
  };
504
+ /** Present, and opaque, only when it has a `trace` (its own or inherited). */
505
+ readonly trace?: {
506
+ readonly kind: "opaque";
507
+ };
488
508
  /** Its publishers (inherited ones included), by key, with the application that declared each; present only when it has some. */
489
509
  readonly publishers?: Readonly<Record<string, {
490
510
  readonly from: string;
@@ -0,0 +1,200 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ import { Unauthenticated } from "../output/exceptions.js";
4
+ export const InvalidJwtOptions = exceptionOf({
5
+ layer: ExceptionLayers.Output,
6
+ name: "InvalidJwtOptions",
7
+ message: ({ reason }) => reason
8
+ });
9
+ const HASHES = {
10
+ HS256: "SHA-256",
11
+ HS384: "SHA-384",
12
+ HS512: "SHA-512"
13
+ };
14
+ const MIN_SECRET = 32;
15
+ const DEFAULT_EXPIRES_IN = 900;
16
+ const REGISTERED = new Set([
17
+ "sub",
18
+ "scope",
19
+ "iat",
20
+ "exp",
21
+ "nbf",
22
+ "iss",
23
+ "aud",
24
+ "use"
25
+ ]);
26
+ const encoder = new TextEncoder;
27
+ const decoder = new TextDecoder;
28
+ function base64url(bytes) {
29
+ let binary = "";
30
+ for (const byte of bytes)
31
+ binary += String.fromCharCode(byte);
32
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
33
+ }
34
+ function bytesOf(text) {
35
+ if (!/^[A-Za-z0-9_-]*$/.test(text))
36
+ return;
37
+ try {
38
+ const binary = atob(text.replaceAll("-", "+").replaceAll("_", "/"));
39
+ const bytes = new Uint8Array(binary.length);
40
+ for (let i = 0;i < binary.length; i++)
41
+ bytes[i] = binary.charCodeAt(i);
42
+ return bytes;
43
+ } catch {
44
+ return;
45
+ }
46
+ }
47
+ function jsonOf(part) {
48
+ const bytes = bytesOf(part);
49
+ if (!bytes)
50
+ return;
51
+ try {
52
+ const value = JSON.parse(decoder.decode(bytes));
53
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ export function jwtOptionsIssue(options, where, extra = [], described = false) {
59
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
60
+ return `Expected ${where} to be { secret, issuer?, audience?, expiresIn?, algorithm? }.`;
61
+ }
62
+ const given = options;
63
+ const known = [
64
+ "secret",
65
+ "issuer",
66
+ "audience",
67
+ "expiresIn",
68
+ "algorithm",
69
+ ...extra
70
+ ];
71
+ for (const key of Object.keys(given)) {
72
+ if (!known.includes(key))
73
+ return `${where} has no option "${key}".`;
74
+ }
75
+ const { secret, issuer, audience, expiresIn, algorithm } = given;
76
+ const named = typeof secret === "object" && secret !== null && !Array.isArray(secret) ? secret : undefined;
77
+ if (named ? Object.keys(named).join() !== "env" || typeof named.env !== "string" || named.env === "" : typeof secret !== "string") {
78
+ return `Expected ${where}.secret to be the secret or { "env": "NAME" }.`;
79
+ }
80
+ if (described && !named) {
81
+ return `Expected ${where}.secret to be { "env": "NAME" }: these options are described, so the secret is named, not written.`;
82
+ }
83
+ if (typeof secret === "string" && secret.length < MIN_SECRET) {
84
+ return `Expected ${where}.secret to have at least ${MIN_SECRET} characters.`;
85
+ }
86
+ for (const [key, value] of [
87
+ ["issuer", issuer],
88
+ ["audience", audience]
89
+ ]) {
90
+ if (value !== undefined && (typeof value !== "string" || value === "")) {
91
+ return `Expected ${where}.${key} to be a non-empty string.`;
92
+ }
93
+ }
94
+ if (expiresIn !== undefined && (!Number.isInteger(expiresIn) || expiresIn <= 0)) {
95
+ return `Expected ${where}.expiresIn to be a whole number of seconds above 0.`;
96
+ }
97
+ if (algorithm !== undefined && !Object.hasOwn(HASHES, algorithm)) {
98
+ return `Expected ${where}.algorithm to be one of ${Object.keys(HASHES).join(", ")}.`;
99
+ }
100
+ return;
101
+ }
102
+ export function jwtOf(options) {
103
+ const issue = jwtOptionsIssue(options, "jwtOf");
104
+ if (issue) {
105
+ throw new InvalidJwtOptions({
106
+ source: "jwtOf",
107
+ details: { reason: issue }
108
+ });
109
+ }
110
+ const algorithm = options.algorithm ?? "HS256";
111
+ const lifetime = options.expiresIn ?? DEFAULT_EXPIRES_IN;
112
+ const header = base64url(encoder.encode(JSON.stringify({ alg: algorithm, typ: "JWT" })));
113
+ let imported;
114
+ const keyOf = () => {
115
+ if (imported)
116
+ return imported;
117
+ let secret;
118
+ if (typeof options.secret === "string")
119
+ secret = options.secret;
120
+ else {
121
+ const name = options.secret.env;
122
+ secret = globalThis.process?.env?.[name];
123
+ if (typeof secret !== "string" || secret.length < MIN_SECRET) {
124
+ throw new InvalidJwtOptions({
125
+ source: "jwtOf",
126
+ details: {
127
+ reason: `The environment variable ${name} must hold a secret of at least ${MIN_SECRET} characters.`
128
+ }
129
+ });
130
+ }
131
+ }
132
+ imported = crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: HASHES[algorithm] }, false, ["sign", "verify"]);
133
+ return imported;
134
+ };
135
+ const refused = () => {
136
+ throw new Unauthenticated({ source: "jwt" });
137
+ };
138
+ const jwt = {
139
+ async sign(subject, token = {}) {
140
+ const now = Math.floor(Date.now() / 1000);
141
+ const payload = {
142
+ ...subject.claims ?? {},
143
+ sub: subject.subject,
144
+ scope: (subject.scopes ?? []).join(" "),
145
+ iat: now,
146
+ exp: now + (token.expiresIn ?? lifetime),
147
+ ...options.issuer === undefined ? {} : { iss: options.issuer },
148
+ ...options.audience === undefined ? {} : { aud: options.audience },
149
+ ...token.use === undefined ? {} : { use: token.use }
150
+ };
151
+ const signing = `${header}.${base64url(encoder.encode(JSON.stringify(payload)))}`;
152
+ const signature = await crypto.subtle.sign("HMAC", await keyOf(), encoder.encode(signing));
153
+ return `${signing}.${base64url(new Uint8Array(signature))}`;
154
+ },
155
+ async verify(token, expected = {}) {
156
+ if (typeof token !== "string")
157
+ return refused();
158
+ const parts = token.split(".");
159
+ if (parts.length !== 3)
160
+ return refused();
161
+ const [head, body, sign] = parts;
162
+ if (jsonOf(head)?.alg !== algorithm)
163
+ return refused();
164
+ const signature = bytesOf(sign);
165
+ if (!signature)
166
+ return refused();
167
+ const valid = await crypto.subtle.verify("HMAC", await keyOf(), signature, encoder.encode(`${head}.${body}`));
168
+ const payload = valid ? jsonOf(body) : undefined;
169
+ if (!payload)
170
+ return refused();
171
+ const now = Math.floor(Date.now() / 1000);
172
+ const { sub, scope, exp, nbf, iss, aud, use } = payload;
173
+ if (typeof sub !== "string" || sub === "" || typeof exp !== "number" || exp <= now || nbf !== undefined && (typeof nbf !== "number" || nbf > now) || options.issuer !== undefined && iss !== options.issuer || options.audience !== undefined && aud !== options.audience && !(Array.isArray(aud) && aud.includes(options.audience)) || use !== expected.use) {
174
+ return refused();
175
+ }
176
+ const claims = {};
177
+ for (const [key, value] of Object.entries(payload)) {
178
+ if (!REGISTERED.has(key))
179
+ claims[key] = value;
180
+ }
181
+ return {
182
+ subject: sub,
183
+ scopes: typeof scope === "string" && scope !== "" ? scope.split(" ") : [],
184
+ ...Object.keys(claims).length > 0 ? { claims } : {}
185
+ };
186
+ },
187
+ authenticator(reading = {}) {
188
+ const header = (reading.header ?? "authorization").toLowerCase();
189
+ const { cookie } = reading;
190
+ return async (request) => {
191
+ const sent = request.header(header);
192
+ const token = sent !== undefined ? sent.replace(/^Bearer +/i, "").trim() : cookie !== undefined ? request.cookie(cookie) : undefined;
193
+ if (token === undefined || token === "")
194
+ return null;
195
+ return jwt.verify(token);
196
+ };
197
+ }
198
+ };
199
+ return Object.freeze(jwt);
200
+ }
@@ -16,9 +16,17 @@ export interface JwtOptions {
16
16
  readonly issuer?: string;
17
17
  /** The `aud` claim: set when signing, required when verifying. */
18
18
  readonly audience?: string;
19
- /** The seconds a token lives: 900 (15 minutes) by default. */
19
+ /**
20
+ * The seconds a token lives.
21
+ *
22
+ * @default 900
23
+ */
20
24
  readonly expiresIn?: number;
21
- /** `"HS256"` by default. */
25
+ /**
26
+ * The algorithm the tokens are signed and verified with.
27
+ *
28
+ * @default "HS256"
29
+ */
22
30
  readonly algorithm?: JwtAlgorithm;
23
31
  }
24
32
  /** What a token is signed for. */
@@ -0,0 +1,7 @@
1
+ export function blueprintPluginOf(name, properties, factories = {}) {
2
+ return Object.freeze({
3
+ name,
4
+ properties: Object.freeze({ ...properties }),
5
+ factories: Object.freeze({ ...factories })
6
+ });
7
+ }
@@ -0,0 +1,146 @@
1
+ import { Type } from "typebox";
2
+ import { collectionParserOf } from "../parse/parse.js";
3
+ import { isProperty } from "../property/is-property.js";
4
+ import { propertyOf } from "../property/property.js";
5
+ import { rawTypeOf, schemaOf } from "../schema/schema.js";
6
+ import { withStandard } from "../standard/standard.js";
7
+ import { Kind } from "../symbols/kind.js";
8
+ import { callableOf, fieldOf } from "./callable.js";
9
+ import {
10
+ ReservedBlueprintKey,
11
+ UndeclaredBlueprintProperty
12
+ } from "./exceptions.js";
13
+ import { isBlueprint } from "./is-blueprint.js";
14
+ import { isBlueprintList, listOf } from "./list.js";
15
+ import { optionalOf } from "./optional.js";
16
+ import { recordFieldOf } from "./record-field.js";
17
+ import { blueprintOfVariant, isBlueprintVariant, variantOf } from "./variant.js";
18
+ export function blueprintBuilderOf(...base) {
19
+ return (sourcesOrDefine, maybeDefine) => {
20
+ const [sources, define] = typeof sourcesOrDefine === "function" ? [[], sourcesOrDefine] : [
21
+ sourcesOrDefine,
22
+ maybeDefine
23
+ ];
24
+ const all = [...base, ...sources];
25
+ return create(all.flatMap((source) => isBlueprint(source) ? source.plugins : [source]), { ...define(contextOf(all)) });
26
+ };
27
+ }
28
+ export function blueprintFromProperties(plugins, properties) {
29
+ return create(plugins, { ...properties });
30
+ }
31
+ function create(plugins, properties) {
32
+ const shape = {};
33
+ for (const [key, field] of Object.entries(properties)) {
34
+ properties[key] = fieldOf(field);
35
+ shape[key] = schemaOfField(key, properties[key]);
36
+ }
37
+ const SchemaConstructor = schemaOf("blueprint", Type.Object(shape));
38
+ const safeParse = collectionParserOf(properties, "blueprint");
39
+ const blueprint = {
40
+ [Kind]: "blueprint",
41
+ plugins: Object.freeze([...new Set(plugins)]),
42
+ properties: Object.freeze(properties),
43
+ schema: new SchemaConstructor,
44
+ optional: false,
45
+ safeParse,
46
+ join: (...others) => create([blueprint, ...others].flatMap((source) => source.plugins), Object.assign({}, ...[blueprint, ...others].map((source) => source.properties)))
47
+ };
48
+ return Object.freeze(withStandard(blueprint, safeParse));
49
+ }
50
+ const HELPERS = { listOf, optionalOf, propertyOf, variantOf };
51
+ const RESERVED_KEYS = ["optional", "list", ...Object.keys(HELPERS)];
52
+ function lazyMirror(keys, make) {
53
+ const fields = {};
54
+ for (const key of keys) {
55
+ let field;
56
+ Object.defineProperty(fields, key, {
57
+ get: () => {
58
+ field ??= make(key);
59
+ return field;
60
+ },
61
+ enumerable: true
62
+ });
63
+ }
64
+ return fields;
65
+ }
66
+ function contextOf(sources) {
67
+ for (const key of RESERVED_KEYS) {
68
+ const reserving = sources.find((source) => Object.hasOwn(source.properties, key));
69
+ if (reserving) {
70
+ throw new ReservedBlueprintKey({
71
+ source: isBlueprint(reserving) ? "blueprint" : reserving.name,
72
+ details: { key }
73
+ });
74
+ }
75
+ }
76
+ const factories = Object.assign({}, ...sources.map((source) => isBlueprint(source) ? {} : source.factories ?? {}));
77
+ for (const key of RESERVED_KEYS) {
78
+ if (Object.hasOwn(factories, key)) {
79
+ const reserving = sources.find((source) => !isBlueprint(source) && Object.hasOwn(source.factories ?? {}, key));
80
+ throw new ReservedBlueprintKey({
81
+ source: reserving.name,
82
+ details: { key }
83
+ });
84
+ }
85
+ }
86
+ const available = Object.assign({}, ...sources.map((source) => source.properties));
87
+ for (const key of Object.keys(factories))
88
+ delete available[key];
89
+ const mirror = (fields, map) => Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, map(field)]));
90
+ const callable = (field) => isProperty(field) ? callableOf(field) : field;
91
+ const keys = Object.keys(available);
92
+ const list = lazyMirror(keys, (key) => listOf(available[key]));
93
+ const optional = lazyMirror(keys, (key) => callable(optionalOf(available[key])));
94
+ const listOptional = Object.freeze(lazyMirror(keys, (key) => optionalOf(list[key])));
95
+ Object.defineProperty(list, "optional", {
96
+ value: listOptional,
97
+ enumerable: true
98
+ });
99
+ return Object.freeze({
100
+ ...mirror(available, callable),
101
+ ...factories,
102
+ ...HELPERS,
103
+ optional: Object.freeze(optional),
104
+ list: Object.freeze(list)
105
+ });
106
+ }
107
+ function schemaOfField(key, field) {
108
+ if (isBlueprintList(field)) {
109
+ const { item, minItems, maxItems } = field;
110
+ const schema = Type.Array(schemaOfField(key, item), {
111
+ ...minItems === undefined ? {} : { minItems },
112
+ ...maxItems === undefined ? {} : { maxItems }
113
+ });
114
+ return field.optional ? Type.Optional(schema) : schema;
115
+ }
116
+ if (isBlueprint(field)) {
117
+ const schema = rawTypeOf(field.schema);
118
+ return field.optional ? Type.Optional(schema) : schema;
119
+ }
120
+ if (isBlueprintVariant(field)) {
121
+ const schema = Type.Union(Object.entries(field.variants).map(([tag, variant]) => {
122
+ const object = rawTypeOf(blueprintOfVariant(variant).schema);
123
+ return {
124
+ ...object,
125
+ properties: {
126
+ [field.key]: Type.Literal(tag),
127
+ ...object.properties
128
+ },
129
+ required: [field.key, ...object.required ?? []]
130
+ };
131
+ }));
132
+ return field.optional ? Type.Optional(schema) : schema;
133
+ }
134
+ const record = recordFieldOf(field);
135
+ if (record) {
136
+ const schema = rawTypeOf(record.record.blueprint.schema);
137
+ return record.optional ? Type.Optional(schema) : schema;
138
+ }
139
+ if (isProperty(field)) {
140
+ return rawTypeOf(field.type.schema);
141
+ }
142
+ throw new UndeclaredBlueprintProperty({
143
+ source: "blueprint",
144
+ details: { key }
145
+ });
146
+ }
@@ -0,0 +1,13 @@
1
+ import { isProperty } from "../property/is-property.js";
2
+ import { refinePropertyOf } from "../property/refine.js";
3
+ const targets = new WeakMap;
4
+ export function callableOf(Property) {
5
+ const callable = new Proxy(Property, {
6
+ apply: (target, _this, [options]) => refinePropertyOf(target, options ?? {})
7
+ });
8
+ targets.set(callable, Property);
9
+ return callable;
10
+ }
11
+ export function fieldOf(field) {
12
+ return (isProperty(field) ? targets.get(field) : undefined) ?? field;
13
+ }
@@ -0,0 +1,20 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ export const ReservedBlueprintKey = exceptionOf({
4
+ layer: ExceptionLayers.Blueprint,
5
+ name: "ReservedBlueprintKey",
6
+ message: ({ key }) => `"${key}" is reserved and cannot be a plugin property.`
7
+ });
8
+ export const InvalidBlueprintVariant = exceptionOf({
9
+ layer: ExceptionLayers.Blueprint,
10
+ name: "InvalidBlueprintVariant",
11
+ message: ({
12
+ key,
13
+ reason
14
+ }) => `The variant field of key "${key}" cannot be declared: ${reason}.`
15
+ });
16
+ export const UndeclaredBlueprintProperty = exceptionOf({
17
+ layer: ExceptionLayers.Blueprint,
18
+ name: "UndeclaredBlueprintProperty",
19
+ message: ({ key }) => `Property "${key}" of the blueprint is not declared.`
20
+ });
@@ -0,0 +1,4 @@
1
+ import { Kind } from "../symbols/kind.js";
2
+ export function isBlueprint(value) {
3
+ return typeof value === "object" && value !== null && value[Kind] === "blueprint";
4
+ }
@@ -0,0 +1,19 @@
1
+ import { parseField } from "../parse/parse.js";
2
+ import { withStandard } from "../standard/standard.js";
3
+ import { Kind } from "../symbols/kind.js";
4
+ import { fieldOf } from "./callable.js";
5
+ export function listOf(item, { minItems, maxItems } = {}) {
6
+ return standardListOf({
7
+ [Kind]: "list",
8
+ item: fieldOf(item),
9
+ optional: false,
10
+ minItems,
11
+ maxItems
12
+ });
13
+ }
14
+ export function standardListOf(list) {
15
+ return Object.freeze(withStandard(list, (value) => parseField(list, value, [], "list")));
16
+ }
17
+ export function isBlueprintList(value) {
18
+ return typeof value === "object" && value !== null && value[Kind] === "list";
19
+ }