@birtalanrobert/context 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/NOTICE +45 -0
- package/README.md +3 -0
- package/dist/context.d.ts +67 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +166 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
- package/src/context.ts +179 -0
- package/src/index.ts +22 -0
- package/src/types.ts +49 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
mortar
|
|
2
|
+
Copyright (C) 2026 Robert Birtalan
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
the terms of the GNU Affero General Public License as published by the Free
|
|
6
|
+
Software Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
10
|
+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
11
|
+
PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU Affero General Public License along
|
|
14
|
+
with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
--------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
RIGHTS RESERVED BY THE COPYRIGHT HOLDER
|
|
19
|
+
|
|
20
|
+
The AGPL is a grant from the copyright holder to everyone else. The copyright
|
|
21
|
+
holder is not a licensee of their own work, and is therefore not bound by these
|
|
22
|
+
terms.
|
|
23
|
+
|
|
24
|
+
Robert Birtalan, as sole copyright holder, retains the right to use, modify and
|
|
25
|
+
distribute this software under any other terms, including within closed-source
|
|
26
|
+
and commercial products, without triggering any obligation under the AGPL.
|
|
27
|
+
|
|
28
|
+
Commercial licences for third parties who cannot or do not wish to comply with
|
|
29
|
+
the AGPL are available on request.
|
|
30
|
+
|
|
31
|
+
--------------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
CONTRIBUTIONS
|
|
34
|
+
|
|
35
|
+
Contributions are not currently accepted.
|
|
36
|
+
|
|
37
|
+
This is deliberate and load-bearing. A contributor retains copyright in their
|
|
38
|
+
contribution unless they assign it. Accepting outside contributions without a
|
|
39
|
+
copyright assignment or a contributor licence agreement would mean the project
|
|
40
|
+
was no longer wholly owned by one copyright holder — and the reserved rights
|
|
41
|
+
above would no longer apply to those parts, because one cannot relicense code
|
|
42
|
+
one does not own.
|
|
43
|
+
|
|
44
|
+
Should contributions be accepted in future, a CLA assigning copyright to the
|
|
45
|
+
maintainer is a prerequisite, not a formality.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Actor, ContextSource, RequestContext } from './types';
|
|
2
|
+
export interface CreateContextOptions {
|
|
3
|
+
requestId?: string;
|
|
4
|
+
correlationId?: string;
|
|
5
|
+
tenantId?: string;
|
|
6
|
+
actor?: Actor;
|
|
7
|
+
locale?: string;
|
|
8
|
+
ip?: string;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
source?: ContextSource;
|
|
11
|
+
}
|
|
12
|
+
/** Builds a context without entering it. Rarely needed directly. */
|
|
13
|
+
export declare function createContext(options?: CreateContextOptions): RequestContext;
|
|
14
|
+
/**
|
|
15
|
+
* Runs `fn` inside a fresh context. Everything called from `fn`, synchronously
|
|
16
|
+
* or asynchronously, sees that context.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runInContext<T>(options: CreateContextOptions, fn: () => T): T;
|
|
19
|
+
/** Runs `fn` inside an already-built context. */
|
|
20
|
+
export declare function runWithContext<T>(context: RequestContext, fn: () => T): T;
|
|
21
|
+
/** The current context, or undefined outside one. */
|
|
22
|
+
export declare function getContext(): RequestContext | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The current context, throwing if there is none.
|
|
25
|
+
*
|
|
26
|
+
* Use where the absence of a context is a programming error — a tenant-scoped
|
|
27
|
+
* repository, for instance, must never run unscoped.
|
|
28
|
+
*/
|
|
29
|
+
export declare function requireContext(): RequestContext;
|
|
30
|
+
/** The current tenant, or undefined. */
|
|
31
|
+
export declare function getTenantId(): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The current tenant, throwing if absent.
|
|
34
|
+
*
|
|
35
|
+
* This is a security primitive: a query that should be tenant-scoped must fail
|
|
36
|
+
* loudly rather than quietly returning every tenant's rows.
|
|
37
|
+
*/
|
|
38
|
+
export declare function requireTenantId(): string;
|
|
39
|
+
export declare function getActor(): Actor | undefined;
|
|
40
|
+
export declare function requireActor(): Actor;
|
|
41
|
+
export declare function getRequestId(): string | undefined;
|
|
42
|
+
export declare function getCorrelationId(): string | undefined;
|
|
43
|
+
export declare function getLocale(): string | undefined;
|
|
44
|
+
/** Milliseconds since the current unit of work started. */
|
|
45
|
+
export declare function elapsedMs(): number | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Mutates the active context.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately narrow: only the fields that are genuinely resolved *during* a
|
|
50
|
+
* request — the tenant after resolution, the actor after authentication, the
|
|
51
|
+
* locale after negotiation — are settable. Identity fields are immutable.
|
|
52
|
+
*/
|
|
53
|
+
export declare function setContextValues(values: Pick<RequestContext, 'tenantId' | 'actor' | 'locale'>): void;
|
|
54
|
+
export declare function setAttribute(key: string, value: unknown): void;
|
|
55
|
+
export declare function getAttribute<T = unknown>(key: string): T | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Runs `fn` in a child context inheriting correlation from the current one.
|
|
58
|
+
*
|
|
59
|
+
* The mechanism by which a background job spawned from a request stays
|
|
60
|
+
* traceable back to it.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runInChildContext<T>(options: CreateContextOptions, fn: () => T): T;
|
|
63
|
+
/**
|
|
64
|
+
* The fields worth attaching to every log line and propagating to a job.
|
|
65
|
+
*/
|
|
66
|
+
export declare function contextSnapshot(): Record<string, unknown>;
|
|
67
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAIpE,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,cAAc,CAchF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE7E;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEzE;AAED,qDAAqD;AACrD,wBAAgB,UAAU,IAAI,cAAc,GAAG,SAAS,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,cAAc,CAS/C;AAED,wCAAwC;AACxC,wBAAgB,WAAW,IAAI,MAAM,GAAG,SAAS,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CASxC;AAED,wBAAgB,QAAQ,IAAI,KAAK,GAAG,SAAS,CAE5C;AAED,wBAAgB,YAAY,IAAI,KAAK,CAIpC;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAErD;AAED,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAE9C;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAG9C;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC,GAC5D,IAAI,CAMN;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAE9D;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAYlF;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWzD"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createContext = createContext;
|
|
4
|
+
exports.runInContext = runInContext;
|
|
5
|
+
exports.runWithContext = runWithContext;
|
|
6
|
+
exports.getContext = getContext;
|
|
7
|
+
exports.requireContext = requireContext;
|
|
8
|
+
exports.getTenantId = getTenantId;
|
|
9
|
+
exports.requireTenantId = requireTenantId;
|
|
10
|
+
exports.getActor = getActor;
|
|
11
|
+
exports.requireActor = requireActor;
|
|
12
|
+
exports.getRequestId = getRequestId;
|
|
13
|
+
exports.getCorrelationId = getCorrelationId;
|
|
14
|
+
exports.getLocale = getLocale;
|
|
15
|
+
exports.elapsedMs = elapsedMs;
|
|
16
|
+
exports.setContextValues = setContextValues;
|
|
17
|
+
exports.setAttribute = setAttribute;
|
|
18
|
+
exports.getAttribute = getAttribute;
|
|
19
|
+
exports.runInChildContext = runInChildContext;
|
|
20
|
+
exports.contextSnapshot = contextSnapshot;
|
|
21
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
22
|
+
const node_crypto_1 = require("node:crypto");
|
|
23
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
24
|
+
/** Builds a context without entering it. Rarely needed directly. */
|
|
25
|
+
function createContext(options = {}) {
|
|
26
|
+
const requestId = options.requestId ?? (0, node_crypto_1.randomUUID)();
|
|
27
|
+
return {
|
|
28
|
+
requestId,
|
|
29
|
+
correlationId: options.correlationId ?? requestId,
|
|
30
|
+
tenantId: options.tenantId,
|
|
31
|
+
actor: options.actor,
|
|
32
|
+
locale: options.locale,
|
|
33
|
+
ip: options.ip,
|
|
34
|
+
userAgent: options.userAgent,
|
|
35
|
+
source: options.source ?? 'internal',
|
|
36
|
+
startedAt: Date.now(),
|
|
37
|
+
attributes: new Map(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs `fn` inside a fresh context. Everything called from `fn`, synchronously
|
|
42
|
+
* or asynchronously, sees that context.
|
|
43
|
+
*/
|
|
44
|
+
function runInContext(options, fn) {
|
|
45
|
+
return storage.run(createContext(options), fn);
|
|
46
|
+
}
|
|
47
|
+
/** Runs `fn` inside an already-built context. */
|
|
48
|
+
function runWithContext(context, fn) {
|
|
49
|
+
return storage.run(context, fn);
|
|
50
|
+
}
|
|
51
|
+
/** The current context, or undefined outside one. */
|
|
52
|
+
function getContext() {
|
|
53
|
+
return storage.getStore();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The current context, throwing if there is none.
|
|
57
|
+
*
|
|
58
|
+
* Use where the absence of a context is a programming error — a tenant-scoped
|
|
59
|
+
* repository, for instance, must never run unscoped.
|
|
60
|
+
*/
|
|
61
|
+
function requireContext() {
|
|
62
|
+
const context = storage.getStore();
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new Error('No request context is active. Wrap this work in runInContext(), or use getContext() ' +
|
|
65
|
+
'if running outside a request is legitimate here.');
|
|
66
|
+
}
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
/** The current tenant, or undefined. */
|
|
70
|
+
function getTenantId() {
|
|
71
|
+
return storage.getStore()?.tenantId;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The current tenant, throwing if absent.
|
|
75
|
+
*
|
|
76
|
+
* This is a security primitive: a query that should be tenant-scoped must fail
|
|
77
|
+
* loudly rather than quietly returning every tenant's rows.
|
|
78
|
+
*/
|
|
79
|
+
function requireTenantId() {
|
|
80
|
+
const tenantId = storage.getStore()?.tenantId;
|
|
81
|
+
if (!tenantId) {
|
|
82
|
+
throw new Error('No tenant is bound to the current context. A tenant-scoped operation cannot proceed ' +
|
|
83
|
+
'without one — this would otherwise read across tenants.');
|
|
84
|
+
}
|
|
85
|
+
return tenantId;
|
|
86
|
+
}
|
|
87
|
+
function getActor() {
|
|
88
|
+
return storage.getStore()?.actor;
|
|
89
|
+
}
|
|
90
|
+
function requireActor() {
|
|
91
|
+
const actor = storage.getStore()?.actor;
|
|
92
|
+
if (!actor)
|
|
93
|
+
throw new Error('No actor is bound to the current context.');
|
|
94
|
+
return actor;
|
|
95
|
+
}
|
|
96
|
+
function getRequestId() {
|
|
97
|
+
return storage.getStore()?.requestId;
|
|
98
|
+
}
|
|
99
|
+
function getCorrelationId() {
|
|
100
|
+
return storage.getStore()?.correlationId;
|
|
101
|
+
}
|
|
102
|
+
function getLocale() {
|
|
103
|
+
return storage.getStore()?.locale;
|
|
104
|
+
}
|
|
105
|
+
/** Milliseconds since the current unit of work started. */
|
|
106
|
+
function elapsedMs() {
|
|
107
|
+
const context = storage.getStore();
|
|
108
|
+
return context ? Date.now() - context.startedAt : undefined;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Mutates the active context.
|
|
112
|
+
*
|
|
113
|
+
* Deliberately narrow: only the fields that are genuinely resolved *during* a
|
|
114
|
+
* request — the tenant after resolution, the actor after authentication, the
|
|
115
|
+
* locale after negotiation — are settable. Identity fields are immutable.
|
|
116
|
+
*/
|
|
117
|
+
function setContextValues(values) {
|
|
118
|
+
const context = storage.getStore();
|
|
119
|
+
if (!context)
|
|
120
|
+
return;
|
|
121
|
+
if (values.tenantId !== undefined)
|
|
122
|
+
context.tenantId = values.tenantId;
|
|
123
|
+
if (values.actor !== undefined)
|
|
124
|
+
context.actor = values.actor;
|
|
125
|
+
if (values.locale !== undefined)
|
|
126
|
+
context.locale = values.locale;
|
|
127
|
+
}
|
|
128
|
+
function setAttribute(key, value) {
|
|
129
|
+
storage.getStore()?.attributes.set(key, value);
|
|
130
|
+
}
|
|
131
|
+
function getAttribute(key) {
|
|
132
|
+
return storage.getStore()?.attributes.get(key);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Runs `fn` in a child context inheriting correlation from the current one.
|
|
136
|
+
*
|
|
137
|
+
* The mechanism by which a background job spawned from a request stays
|
|
138
|
+
* traceable back to it.
|
|
139
|
+
*/
|
|
140
|
+
function runInChildContext(options, fn) {
|
|
141
|
+
const parent = storage.getStore();
|
|
142
|
+
return runInContext({
|
|
143
|
+
correlationId: parent?.correlationId,
|
|
144
|
+
tenantId: parent?.tenantId,
|
|
145
|
+
actor: parent?.actor,
|
|
146
|
+
locale: parent?.locale,
|
|
147
|
+
...options,
|
|
148
|
+
}, fn);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The fields worth attaching to every log line and propagating to a job.
|
|
152
|
+
*/
|
|
153
|
+
function contextSnapshot() {
|
|
154
|
+
const context = storage.getStore();
|
|
155
|
+
if (!context)
|
|
156
|
+
return {};
|
|
157
|
+
return {
|
|
158
|
+
requestId: context.requestId,
|
|
159
|
+
correlationId: context.correlationId,
|
|
160
|
+
...(context.tenantId ? { tenantId: context.tenantId } : {}),
|
|
161
|
+
...(context.actor ? { actorId: context.actor.id, actorType: context.actor.type } : {}),
|
|
162
|
+
...(context.actor?.impersonatedBy ? { impersonatedBy: context.actor.impersonatedBy } : {}),
|
|
163
|
+
source: context.source,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { Actor, ContextSource, RequestContext } from './types';
|
|
2
|
+
export { contextSnapshot, createContext, elapsedMs, getActor, getAttribute, getContext, getCorrelationId, getLocale, getRequestId, getTenantId, requireActor, requireContext, requireTenantId, runInChildContext, runInContext, runWithContext, setAttribute, setContextValues, type CreateContextOptions, } from './context';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EACL,eAAe,EACf,aAAa,EACb,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setContextValues = exports.setAttribute = exports.runWithContext = exports.runInContext = exports.runInChildContext = exports.requireTenantId = exports.requireContext = exports.requireActor = exports.getTenantId = exports.getRequestId = exports.getLocale = exports.getCorrelationId = exports.getContext = exports.getAttribute = exports.getActor = exports.elapsedMs = exports.createContext = exports.contextSnapshot = void 0;
|
|
4
|
+
var context_1 = require("./context");
|
|
5
|
+
Object.defineProperty(exports, "contextSnapshot", { enumerable: true, get: function () { return context_1.contextSnapshot; } });
|
|
6
|
+
Object.defineProperty(exports, "createContext", { enumerable: true, get: function () { return context_1.createContext; } });
|
|
7
|
+
Object.defineProperty(exports, "elapsedMs", { enumerable: true, get: function () { return context_1.elapsedMs; } });
|
|
8
|
+
Object.defineProperty(exports, "getActor", { enumerable: true, get: function () { return context_1.getActor; } });
|
|
9
|
+
Object.defineProperty(exports, "getAttribute", { enumerable: true, get: function () { return context_1.getAttribute; } });
|
|
10
|
+
Object.defineProperty(exports, "getContext", { enumerable: true, get: function () { return context_1.getContext; } });
|
|
11
|
+
Object.defineProperty(exports, "getCorrelationId", { enumerable: true, get: function () { return context_1.getCorrelationId; } });
|
|
12
|
+
Object.defineProperty(exports, "getLocale", { enumerable: true, get: function () { return context_1.getLocale; } });
|
|
13
|
+
Object.defineProperty(exports, "getRequestId", { enumerable: true, get: function () { return context_1.getRequestId; } });
|
|
14
|
+
Object.defineProperty(exports, "getTenantId", { enumerable: true, get: function () { return context_1.getTenantId; } });
|
|
15
|
+
Object.defineProperty(exports, "requireActor", { enumerable: true, get: function () { return context_1.requireActor; } });
|
|
16
|
+
Object.defineProperty(exports, "requireContext", { enumerable: true, get: function () { return context_1.requireContext; } });
|
|
17
|
+
Object.defineProperty(exports, "requireTenantId", { enumerable: true, get: function () { return context_1.requireTenantId; } });
|
|
18
|
+
Object.defineProperty(exports, "runInChildContext", { enumerable: true, get: function () { return context_1.runInChildContext; } });
|
|
19
|
+
Object.defineProperty(exports, "runInContext", { enumerable: true, get: function () { return context_1.runInContext; } });
|
|
20
|
+
Object.defineProperty(exports, "runWithContext", { enumerable: true, get: function () { return context_1.runWithContext; } });
|
|
21
|
+
Object.defineProperty(exports, "setAttribute", { enumerable: true, get: function () { return context_1.setAttribute; } });
|
|
22
|
+
Object.defineProperty(exports, "setContextValues", { enumerable: true, get: function () { return context_1.setContextValues; } });
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,qCAoBmB;AAnBjB,0GAAA,eAAe,OAAA;AACf,wGAAA,aAAa,OAAA;AACb,oGAAA,SAAS,OAAA;AACT,mGAAA,QAAQ,OAAA;AACR,uGAAA,YAAY,OAAA;AACZ,qGAAA,UAAU,OAAA;AACV,2GAAA,gBAAgB,OAAA;AAChB,oGAAA,SAAS,OAAA;AACT,uGAAA,YAAY,OAAA;AACZ,sGAAA,WAAW,OAAA;AACX,uGAAA,YAAY,OAAA;AACZ,yGAAA,cAAc,OAAA;AACd,0GAAA,eAAe,OAAA;AACf,4GAAA,iBAAiB,OAAA;AACjB,uGAAA,YAAY,OAAA;AACZ,yGAAA,cAAc,OAAA;AACd,uGAAA,YAAY,OAAA;AACZ,2GAAA,gBAAgB,OAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient facts about the work currently in flight.
|
|
3
|
+
*
|
|
4
|
+
* Carried through AsyncLocalStorage rather than threaded through every
|
|
5
|
+
* function signature, because these values are needed by logging, auditing,
|
|
6
|
+
* tenant scoping and error reporting — layers that should not force every
|
|
7
|
+
* intermediate function to know about them.
|
|
8
|
+
*/
|
|
9
|
+
export interface RequestContext {
|
|
10
|
+
/** Unique per inbound request or job execution. */
|
|
11
|
+
readonly requestId: string;
|
|
12
|
+
/**
|
|
13
|
+
* Correlates work across service boundaries and background jobs. Inherited
|
|
14
|
+
* from an incoming header where present, otherwise equal to requestId.
|
|
15
|
+
*/
|
|
16
|
+
readonly correlationId: string;
|
|
17
|
+
/** The tenant this work belongs to, once resolved. */
|
|
18
|
+
tenantId?: string;
|
|
19
|
+
/** The authenticated principal, once resolved. */
|
|
20
|
+
actor?: Actor;
|
|
21
|
+
/** Resolved locale, e.g. 'ro-RO'. */
|
|
22
|
+
locale?: string;
|
|
23
|
+
/** Client address, for audit and rate limiting. */
|
|
24
|
+
ip?: string;
|
|
25
|
+
/** Client user agent, for audit. */
|
|
26
|
+
userAgent?: string;
|
|
27
|
+
/** Where this unit of work came from. */
|
|
28
|
+
readonly source: ContextSource;
|
|
29
|
+
/** When the unit of work started, for duration measurement. */
|
|
30
|
+
readonly startedAt: number;
|
|
31
|
+
/** Free-form values attached by application code. */
|
|
32
|
+
readonly attributes: Map<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
export type ContextSource = 'http' | 'job' | 'cli' | 'test' | 'internal';
|
|
35
|
+
export interface Actor {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
/**
|
|
38
|
+
* `user` is a human with an account; `client` is a link-authenticated party
|
|
39
|
+
* with no account (a guest, a candidate, a tenant of a landlord); `system`
|
|
40
|
+
* is scheduled or internal work; `service` is a machine credential.
|
|
41
|
+
*/
|
|
42
|
+
readonly type: 'user' | 'client' | 'system' | 'service';
|
|
43
|
+
readonly displayName?: string;
|
|
44
|
+
readonly roles?: readonly string[];
|
|
45
|
+
/** Set when an operator is impersonating; the audit trail records both. */
|
|
46
|
+
readonly impersonatedBy?: string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzE,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@birtalanrobert/context",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AsyncLocalStorage request context",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"!src/**/*.test.ts",
|
|
13
|
+
"!src/**/*.spec.ts",
|
|
14
|
+
"!src/**/__tests__",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/birtalanrobert/mortar.git",
|
|
32
|
+
"directory": "packages/context"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import type { Actor, ContextSource, RequestContext } from './types';
|
|
4
|
+
|
|
5
|
+
const storage = new AsyncLocalStorage<RequestContext>();
|
|
6
|
+
|
|
7
|
+
export interface CreateContextOptions {
|
|
8
|
+
requestId?: string;
|
|
9
|
+
correlationId?: string;
|
|
10
|
+
tenantId?: string;
|
|
11
|
+
actor?: Actor;
|
|
12
|
+
locale?: string;
|
|
13
|
+
ip?: string;
|
|
14
|
+
userAgent?: string;
|
|
15
|
+
source?: ContextSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Builds a context without entering it. Rarely needed directly. */
|
|
19
|
+
export function createContext(options: CreateContextOptions = {}): RequestContext {
|
|
20
|
+
const requestId = options.requestId ?? randomUUID();
|
|
21
|
+
return {
|
|
22
|
+
requestId,
|
|
23
|
+
correlationId: options.correlationId ?? requestId,
|
|
24
|
+
tenantId: options.tenantId,
|
|
25
|
+
actor: options.actor,
|
|
26
|
+
locale: options.locale,
|
|
27
|
+
ip: options.ip,
|
|
28
|
+
userAgent: options.userAgent,
|
|
29
|
+
source: options.source ?? 'internal',
|
|
30
|
+
startedAt: Date.now(),
|
|
31
|
+
attributes: new Map<string, unknown>(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runs `fn` inside a fresh context. Everything called from `fn`, synchronously
|
|
37
|
+
* or asynchronously, sees that context.
|
|
38
|
+
*/
|
|
39
|
+
export function runInContext<T>(options: CreateContextOptions, fn: () => T): T {
|
|
40
|
+
return storage.run(createContext(options), fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Runs `fn` inside an already-built context. */
|
|
44
|
+
export function runWithContext<T>(context: RequestContext, fn: () => T): T {
|
|
45
|
+
return storage.run(context, fn);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The current context, or undefined outside one. */
|
|
49
|
+
export function getContext(): RequestContext | undefined {
|
|
50
|
+
return storage.getStore();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The current context, throwing if there is none.
|
|
55
|
+
*
|
|
56
|
+
* Use where the absence of a context is a programming error — a tenant-scoped
|
|
57
|
+
* repository, for instance, must never run unscoped.
|
|
58
|
+
*/
|
|
59
|
+
export function requireContext(): RequestContext {
|
|
60
|
+
const context = storage.getStore();
|
|
61
|
+
if (!context) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'No request context is active. Wrap this work in runInContext(), or use getContext() ' +
|
|
64
|
+
'if running outside a request is legitimate here.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The current tenant, or undefined. */
|
|
71
|
+
export function getTenantId(): string | undefined {
|
|
72
|
+
return storage.getStore()?.tenantId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The current tenant, throwing if absent.
|
|
77
|
+
*
|
|
78
|
+
* This is a security primitive: a query that should be tenant-scoped must fail
|
|
79
|
+
* loudly rather than quietly returning every tenant's rows.
|
|
80
|
+
*/
|
|
81
|
+
export function requireTenantId(): string {
|
|
82
|
+
const tenantId = storage.getStore()?.tenantId;
|
|
83
|
+
if (!tenantId) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'No tenant is bound to the current context. A tenant-scoped operation cannot proceed ' +
|
|
86
|
+
'without one — this would otherwise read across tenants.',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return tenantId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getActor(): Actor | undefined {
|
|
93
|
+
return storage.getStore()?.actor;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function requireActor(): Actor {
|
|
97
|
+
const actor = storage.getStore()?.actor;
|
|
98
|
+
if (!actor) throw new Error('No actor is bound to the current context.');
|
|
99
|
+
return actor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getRequestId(): string | undefined {
|
|
103
|
+
return storage.getStore()?.requestId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getCorrelationId(): string | undefined {
|
|
107
|
+
return storage.getStore()?.correlationId;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getLocale(): string | undefined {
|
|
111
|
+
return storage.getStore()?.locale;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Milliseconds since the current unit of work started. */
|
|
115
|
+
export function elapsedMs(): number | undefined {
|
|
116
|
+
const context = storage.getStore();
|
|
117
|
+
return context ? Date.now() - context.startedAt : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Mutates the active context.
|
|
122
|
+
*
|
|
123
|
+
* Deliberately narrow: only the fields that are genuinely resolved *during* a
|
|
124
|
+
* request — the tenant after resolution, the actor after authentication, the
|
|
125
|
+
* locale after negotiation — are settable. Identity fields are immutable.
|
|
126
|
+
*/
|
|
127
|
+
export function setContextValues(
|
|
128
|
+
values: Pick<RequestContext, 'tenantId' | 'actor' | 'locale'>,
|
|
129
|
+
): void {
|
|
130
|
+
const context = storage.getStore();
|
|
131
|
+
if (!context) return;
|
|
132
|
+
if (values.tenantId !== undefined) context.tenantId = values.tenantId;
|
|
133
|
+
if (values.actor !== undefined) context.actor = values.actor;
|
|
134
|
+
if (values.locale !== undefined) context.locale = values.locale;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function setAttribute(key: string, value: unknown): void {
|
|
138
|
+
storage.getStore()?.attributes.set(key, value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getAttribute<T = unknown>(key: string): T | undefined {
|
|
142
|
+
return storage.getStore()?.attributes.get(key) as T | undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Runs `fn` in a child context inheriting correlation from the current one.
|
|
147
|
+
*
|
|
148
|
+
* The mechanism by which a background job spawned from a request stays
|
|
149
|
+
* traceable back to it.
|
|
150
|
+
*/
|
|
151
|
+
export function runInChildContext<T>(options: CreateContextOptions, fn: () => T): T {
|
|
152
|
+
const parent = storage.getStore();
|
|
153
|
+
return runInContext(
|
|
154
|
+
{
|
|
155
|
+
correlationId: parent?.correlationId,
|
|
156
|
+
tenantId: parent?.tenantId,
|
|
157
|
+
actor: parent?.actor,
|
|
158
|
+
locale: parent?.locale,
|
|
159
|
+
...options,
|
|
160
|
+
},
|
|
161
|
+
fn,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The fields worth attaching to every log line and propagating to a job.
|
|
167
|
+
*/
|
|
168
|
+
export function contextSnapshot(): Record<string, unknown> {
|
|
169
|
+
const context = storage.getStore();
|
|
170
|
+
if (!context) return {};
|
|
171
|
+
return {
|
|
172
|
+
requestId: context.requestId,
|
|
173
|
+
correlationId: context.correlationId,
|
|
174
|
+
...(context.tenantId ? { tenantId: context.tenantId } : {}),
|
|
175
|
+
...(context.actor ? { actorId: context.actor.id, actorType: context.actor.type } : {}),
|
|
176
|
+
...(context.actor?.impersonatedBy ? { impersonatedBy: context.actor.impersonatedBy } : {}),
|
|
177
|
+
source: context.source,
|
|
178
|
+
};
|
|
179
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { Actor, ContextSource, RequestContext } from './types';
|
|
2
|
+
export {
|
|
3
|
+
contextSnapshot,
|
|
4
|
+
createContext,
|
|
5
|
+
elapsedMs,
|
|
6
|
+
getActor,
|
|
7
|
+
getAttribute,
|
|
8
|
+
getContext,
|
|
9
|
+
getCorrelationId,
|
|
10
|
+
getLocale,
|
|
11
|
+
getRequestId,
|
|
12
|
+
getTenantId,
|
|
13
|
+
requireActor,
|
|
14
|
+
requireContext,
|
|
15
|
+
requireTenantId,
|
|
16
|
+
runInChildContext,
|
|
17
|
+
runInContext,
|
|
18
|
+
runWithContext,
|
|
19
|
+
setAttribute,
|
|
20
|
+
setContextValues,
|
|
21
|
+
type CreateContextOptions,
|
|
22
|
+
} from './context';
|