@furystack/core 15.0.35 โ 15.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/CHANGELOG.md +44 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/models/physical-store.d.ts +8 -1
- package/esm/models/physical-store.d.ts.map +1 -1
- package/esm/system-identity-context.d.ts +68 -0
- package/esm/system-identity-context.d.ts.map +1 -0
- package/esm/system-identity-context.js +75 -0
- package/esm/system-identity-context.js.map +1 -0
- package/esm/system-identity-context.spec.d.ts +2 -0
- package/esm/system-identity-context.spec.d.ts.map +1 -0
- package/esm/system-identity-context.spec.js +90 -0
- package/esm/system-identity-context.spec.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/models/physical-store.ts +8 -1
- package/src/system-identity-context.spec.ts +101 -0
- package/src/system-identity-context.ts +83 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [15.1.0] - 2026-02-19
|
|
4
|
+
|
|
5
|
+
### โจ Features
|
|
6
|
+
|
|
7
|
+
### `SystemIdentityContext` -- elevated identity for trusted server-side operations
|
|
8
|
+
|
|
9
|
+
Added `SystemIdentityContext`, an `IdentityContext` subclass that is always authenticated and authorized. It is intended for background jobs, migrations, and seed scripts that need to write through the `DataSet` layer without an HTTP user session.
|
|
10
|
+
|
|
11
|
+
Also added the `useSystemIdentityContext()` helper that creates a scoped child injector with the elevated context. The returned injector is `AsyncDisposable` and works with `usingAsync()` for automatic cleanup.
|
|
12
|
+
|
|
13
|
+
**Usage:**
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { useSystemIdentityContext } from '@furystack/core'
|
|
17
|
+
import { getDataSetFor } from '@furystack/repository'
|
|
18
|
+
import { usingAsync } from '@furystack/utils'
|
|
19
|
+
|
|
20
|
+
await usingAsync(useSystemIdentityContext({ injector, username: 'migration-job' }), async (systemInjector) => {
|
|
21
|
+
const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
22
|
+
await dataSet.add(systemInjector, newEntity)
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### ๐ Documentation
|
|
27
|
+
|
|
28
|
+
- Expanded JSDoc on `PhysicalStore` to warn that writing directly to the store bypasses DataSet authorization, hooks, and events
|
|
29
|
+
|
|
30
|
+
### ๐งช Tests
|
|
31
|
+
|
|
32
|
+
- Added tests for `SystemIdentityContext` (authentication, authorization, custom username)
|
|
33
|
+
- Added tests for `useSystemIdentityContext` (child injector scoping, disposal, identity resolution)
|
|
34
|
+
|
|
35
|
+
### โฌ๏ธ Dependencies
|
|
36
|
+
|
|
37
|
+
- Updated `@furystack/inject` and `@furystack/utils`
|
|
38
|
+
|
|
39
|
+
## [15.0.36] - 2026-02-11
|
|
40
|
+
|
|
41
|
+
### โฌ๏ธ Dependencies
|
|
42
|
+
|
|
43
|
+
- Bump `vitest` from `^4.0.17` to `^4.0.18`
|
|
44
|
+
- Bump `@types/node` from `^25.0.10` to `^25.2.3`
|
|
45
|
+
- Updated internal dependencies
|
|
46
|
+
|
|
3
47
|
## [15.0.35] - 2026-02-09
|
|
4
48
|
|
|
5
49
|
### ๐ Bug Fixes
|
package/esm/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export * from './in-memory-store.js';
|
|
|
5
5
|
export * from './store-manager.js';
|
|
6
6
|
export * from './global-disposables.js';
|
|
7
7
|
export * from './identity-context.js';
|
|
8
|
+
export * from './system-identity-context.js';
|
|
8
9
|
export * from './helpers.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,cAAc,CAAA"}
|
package/esm/index.js
CHANGED
|
@@ -5,5 +5,6 @@ export * from './in-memory-store.js';
|
|
|
5
5
|
export * from './store-manager.js';
|
|
6
6
|
export * from './global-disposables.js';
|
|
7
7
|
export * from './identity-context.js';
|
|
8
|
+
export * from './system-identity-context.js';
|
|
8
9
|
export * from './helpers.js';
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,cAAc,CAAA"}
|
|
@@ -62,7 +62,14 @@ export interface FindOptions<T, TSelect extends Array<keyof T>> {
|
|
|
62
62
|
export type PartialResult<T, TFields extends Array<keyof T>> = Pick<T, TFields[number]>;
|
|
63
63
|
export declare const selectFields: <T extends object, TField extends Array<keyof T>>(entry: T, ...fields: TField) => PartialResult<T, TField>;
|
|
64
64
|
/**
|
|
65
|
-
* Interface that defines a physical store implementation
|
|
65
|
+
* Interface that defines a physical store implementation.
|
|
66
|
+
*
|
|
67
|
+
* **Important:** Writing directly to a physical store bypasses the Repository {@link DataSet} layer.
|
|
68
|
+
* This means authorization, modification hooks, and DataSet events (used by entity sync) will **not** be triggered.
|
|
69
|
+
* For any write operation that should be observable by other parts of the system (e.g. entity sync, audit logging),
|
|
70
|
+
* use the corresponding {@link DataSet} method instead via `getDataSetFor()`.
|
|
71
|
+
*
|
|
72
|
+
* @see {@link DataSet} for the authorized, event-emitting write gateway
|
|
66
73
|
*/
|
|
67
74
|
export interface PhysicalStore<T, TPrimaryKey extends keyof T, TWriteableData = WithOptionalId<T, TPrimaryKey>> extends EventHub<{
|
|
68
75
|
onEntityAdded: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"physical-store.d.ts","sourceRoot":"","sources":["../../src/models/physical-store.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"physical-store.d.ts","sourceRoot":"","sources":["../../src/models/physical-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Injector } from '@furystack/inject';
|
|
2
|
+
import { IdentityContext } from './identity-context.js';
|
|
3
|
+
import type { User } from './models/user.js';
|
|
4
|
+
/**
|
|
5
|
+
* An elevated {@link IdentityContext} that is always authenticated and authorized.
|
|
6
|
+
* Intended for trusted server-side operations such as background jobs, migrations, and seed scripts.
|
|
7
|
+
*
|
|
8
|
+
* **Warning:** This context bypasses **all** authorization checks. Never use it in user-facing
|
|
9
|
+
* request pipelines or any context where untrusted input could reach the DataSet.
|
|
10
|
+
* Prefer {@link useSystemIdentityContext} for scoped usage with automatic cleanup.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
15
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
16
|
+
* import { usingAsync } from '@furystack/utils'
|
|
17
|
+
*
|
|
18
|
+
* await usingAsync(
|
|
19
|
+
* useSystemIdentityContext({ injector, username: 'migration-job' }),
|
|
20
|
+
* async (systemInjector) => {
|
|
21
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
22
|
+
* await dataSet.add(systemInjector, newEntity)
|
|
23
|
+
* },
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class SystemIdentityContext extends IdentityContext {
|
|
28
|
+
private readonly username;
|
|
29
|
+
constructor(options?: {
|
|
30
|
+
username?: string;
|
|
31
|
+
});
|
|
32
|
+
isAuthenticated(): Promise<boolean>;
|
|
33
|
+
isAuthorized(..._roles: string[]): Promise<boolean>;
|
|
34
|
+
getCurrentUser<TUser extends User>(): Promise<TUser>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a scoped child injector with an elevated {@link SystemIdentityContext}.
|
|
38
|
+
* The returned injector is {@link AsyncDisposable} and works with `usingAsync()` for automatic cleanup.
|
|
39
|
+
*
|
|
40
|
+
* **Warning:** The returned injector bypasses **all** authorization checks. Only use this in trusted
|
|
41
|
+
* server-side contexts (background jobs, migrations, seed scripts). Never pass the returned injector
|
|
42
|
+
* to user-facing request handlers.
|
|
43
|
+
*
|
|
44
|
+
* @param options.injector The parent injector to create a child from
|
|
45
|
+
* @param options.username The username for the system identity (defaults to `'system'`)
|
|
46
|
+
* @returns A child injector with the SystemIdentityContext set as the IdentityContext
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
51
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
52
|
+
* import { usingAsync } from '@furystack/utils'
|
|
53
|
+
*
|
|
54
|
+
* await usingAsync(
|
|
55
|
+
* useSystemIdentityContext({ injector, username: 'seed-script' }),
|
|
56
|
+
* async (systemInjector) => {
|
|
57
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
58
|
+
* await dataSet.add(systemInjector, { value: 'seeded' })
|
|
59
|
+
* },
|
|
60
|
+
* )
|
|
61
|
+
* // systemInjector is disposed here -- all scoped instances cleaned up
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare const useSystemIdentityContext: (options: {
|
|
65
|
+
injector: Injector;
|
|
66
|
+
username?: string;
|
|
67
|
+
}) => Injector;
|
|
68
|
+
//# sourceMappingURL=system-identity-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.d.ts","sourceRoot":"","sources":["../src/system-identity-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,qBAAsB,SAAQ,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAK3B,eAAe;IAIf,YAAY,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE;IAIhC,cAAc,CAAC,KAAK,SAAS,IAAI,KAAK,OAAO,CAAC,KAAK,CAAC;CAGrE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,QAK7F,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IdentityContext } from './identity-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* An elevated {@link IdentityContext} that is always authenticated and authorized.
|
|
4
|
+
* Intended for trusted server-side operations such as background jobs, migrations, and seed scripts.
|
|
5
|
+
*
|
|
6
|
+
* **Warning:** This context bypasses **all** authorization checks. Never use it in user-facing
|
|
7
|
+
* request pipelines or any context where untrusted input could reach the DataSet.
|
|
8
|
+
* Prefer {@link useSystemIdentityContext} for scoped usage with automatic cleanup.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
13
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
14
|
+
* import { usingAsync } from '@furystack/utils'
|
|
15
|
+
*
|
|
16
|
+
* await usingAsync(
|
|
17
|
+
* useSystemIdentityContext({ injector, username: 'migration-job' }),
|
|
18
|
+
* async (systemInjector) => {
|
|
19
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
20
|
+
* await dataSet.add(systemInjector, newEntity)
|
|
21
|
+
* },
|
|
22
|
+
* )
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class SystemIdentityContext extends IdentityContext {
|
|
26
|
+
username;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
super();
|
|
29
|
+
this.username = options?.username ?? 'system';
|
|
30
|
+
}
|
|
31
|
+
isAuthenticated() {
|
|
32
|
+
return Promise.resolve(true);
|
|
33
|
+
}
|
|
34
|
+
isAuthorized(..._roles) {
|
|
35
|
+
return Promise.resolve(true);
|
|
36
|
+
}
|
|
37
|
+
getCurrentUser() {
|
|
38
|
+
return Promise.resolve({ username: this.username, roles: [] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a scoped child injector with an elevated {@link SystemIdentityContext}.
|
|
43
|
+
* The returned injector is {@link AsyncDisposable} and works with `usingAsync()` for automatic cleanup.
|
|
44
|
+
*
|
|
45
|
+
* **Warning:** The returned injector bypasses **all** authorization checks. Only use this in trusted
|
|
46
|
+
* server-side contexts (background jobs, migrations, seed scripts). Never pass the returned injector
|
|
47
|
+
* to user-facing request handlers.
|
|
48
|
+
*
|
|
49
|
+
* @param options.injector The parent injector to create a child from
|
|
50
|
+
* @param options.username The username for the system identity (defaults to `'system'`)
|
|
51
|
+
* @returns A child injector with the SystemIdentityContext set as the IdentityContext
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
56
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
57
|
+
* import { usingAsync } from '@furystack/utils'
|
|
58
|
+
*
|
|
59
|
+
* await usingAsync(
|
|
60
|
+
* useSystemIdentityContext({ injector, username: 'seed-script' }),
|
|
61
|
+
* async (systemInjector) => {
|
|
62
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
63
|
+
* await dataSet.add(systemInjector, { value: 'seeded' })
|
|
64
|
+
* },
|
|
65
|
+
* )
|
|
66
|
+
* // systemInjector is disposed here -- all scoped instances cleaned up
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export const useSystemIdentityContext = (options) => {
|
|
70
|
+
const ctx = new SystemIdentityContext({ username: options.username });
|
|
71
|
+
const childInjector = options.injector.createChild({ owner: 'SystemIdentityContext' });
|
|
72
|
+
childInjector.setExplicitInstance(ctx, IdentityContext);
|
|
73
|
+
return childInjector;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=system-identity-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.js","sourceRoot":"","sources":["../src/system-identity-context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAGvD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,eAAe;IACvC,QAAQ,CAAQ;IAEjC,YAAY,OAA+B;QACzC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,QAAQ,CAAA;IAC/C,CAAC;IAEe,eAAe;QAC7B,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAEe,YAAY,CAAC,GAAG,MAAgB;QAC9C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAEe,cAAc;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAsB,CAAC,CAAA;IACpF,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAAkD,EAAY,EAAE;IACvG,MAAM,GAAG,GAAG,IAAI,qBAAqB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;IACrE,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAA;IACtF,aAAa,CAAC,mBAAmB,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IACvD,OAAO,aAAa,CAAA;AACtB,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.spec.d.ts","sourceRoot":"","sources":["../src/system-identity-context.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject';
|
|
2
|
+
import { usingAsync } from '@furystack/utils';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { getCurrentUser, isAuthenticated, isAuthorized } from './helpers.js';
|
|
5
|
+
import { IdentityContext } from './identity-context.js';
|
|
6
|
+
import { SystemIdentityContext, useSystemIdentityContext } from './system-identity-context.js';
|
|
7
|
+
describe('SystemIdentityContext', () => {
|
|
8
|
+
it('isAuthenticated should return true', async () => {
|
|
9
|
+
const ctx = new SystemIdentityContext();
|
|
10
|
+
expect(await ctx.isAuthenticated()).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it('isAuthorized should return true without roles', async () => {
|
|
13
|
+
const ctx = new SystemIdentityContext();
|
|
14
|
+
expect(await ctx.isAuthorized()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it('isAuthorized should return true with roles', async () => {
|
|
17
|
+
const ctx = new SystemIdentityContext();
|
|
18
|
+
expect(await ctx.isAuthorized('admin', 'superuser')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('getCurrentUser should return default system user', async () => {
|
|
21
|
+
const ctx = new SystemIdentityContext();
|
|
22
|
+
const user = await ctx.getCurrentUser();
|
|
23
|
+
expect(user).toEqual({ username: 'system', roles: [] });
|
|
24
|
+
});
|
|
25
|
+
it('getCurrentUser should respect custom username', async () => {
|
|
26
|
+
const ctx = new SystemIdentityContext({ username: 'migration-job' });
|
|
27
|
+
const user = await ctx.getCurrentUser();
|
|
28
|
+
expect(user).toEqual({ username: 'migration-job', roles: [] });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('useSystemIdentityContext', () => {
|
|
32
|
+
it('should return a child injector, not the parent', async () => {
|
|
33
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
34
|
+
const child = useSystemIdentityContext({ injector: parent });
|
|
35
|
+
expect(child).not.toBe(parent);
|
|
36
|
+
await child[Symbol.asyncDispose]();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('should resolve IdentityContext to a SystemIdentityContext', async () => {
|
|
40
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
41
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
42
|
+
const ctx = child.getInstance(IdentityContext);
|
|
43
|
+
expect(ctx).toBeInstanceOf(SystemIdentityContext);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should be authenticated and authorized via helpers', async () => {
|
|
48
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
49
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
50
|
+
expect(await isAuthenticated(child)).toBe(true);
|
|
51
|
+
expect(await isAuthorized(child, 'admin')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('should return the configured username via getCurrentUser', async () => {
|
|
56
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
57
|
+
await usingAsync(useSystemIdentityContext({ injector: parent, username: 'seed-script' }), async (child) => {
|
|
58
|
+
const user = await getCurrentUser(child);
|
|
59
|
+
expect(user).toEqual({ username: 'seed-script', roles: [] });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it('should use default username when not specified', async () => {
|
|
64
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
65
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
66
|
+
const user = await getCurrentUser(child);
|
|
67
|
+
expect(user.username).toBe('system');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('should dispose the child injector after usingAsync completes', async () => {
|
|
72
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
73
|
+
let childRef;
|
|
74
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
75
|
+
childRef = child;
|
|
76
|
+
});
|
|
77
|
+
expect(childRef).toBeDefined();
|
|
78
|
+
expect(() => childRef.getInstance(IdentityContext)).toThrow('Injector already disposed');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('should not dispose the parent injector', async () => {
|
|
82
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
83
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async () => {
|
|
84
|
+
// no-op
|
|
85
|
+
});
|
|
86
|
+
expect(() => parent.getInstance(IdentityContext)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=system-identity-context.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.spec.js","sourceRoot":"","sources":["../src/system-identity-context.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/core",
|
|
3
|
-
"version": "15.0
|
|
3
|
+
"version": "15.1.0",
|
|
4
4
|
"description": "Core FuryStack package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/furystack/furystack",
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.0
|
|
49
|
+
"@types/node": "^25.3.0",
|
|
50
50
|
"typescript": "^5.9.3",
|
|
51
|
-
"vitest": "^4.0.
|
|
51
|
+
"vitest": "^4.0.18"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@furystack/inject": "^12.0.
|
|
55
|
-
"@furystack/utils": "^8.1.
|
|
54
|
+
"@furystack/inject": "^12.0.30",
|
|
55
|
+
"@furystack/utils": "^8.1.10"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=22.0.0"
|
package/src/index.ts
CHANGED
|
@@ -88,7 +88,14 @@ export const selectFields = <T extends object, TField extends Array<keyof T>>(en
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Interface that defines a physical store implementation
|
|
91
|
+
* Interface that defines a physical store implementation.
|
|
92
|
+
*
|
|
93
|
+
* **Important:** Writing directly to a physical store bypasses the Repository {@link DataSet} layer.
|
|
94
|
+
* This means authorization, modification hooks, and DataSet events (used by entity sync) will **not** be triggered.
|
|
95
|
+
* For any write operation that should be observable by other parts of the system (e.g. entity sync, audit logging),
|
|
96
|
+
* use the corresponding {@link DataSet} method instead via `getDataSetFor()`.
|
|
97
|
+
*
|
|
98
|
+
* @see {@link DataSet} for the authorized, event-emitting write gateway
|
|
92
99
|
*/
|
|
93
100
|
export interface PhysicalStore<
|
|
94
101
|
T,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { usingAsync } from '@furystack/utils'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { getCurrentUser, isAuthenticated, isAuthorized } from './helpers.js'
|
|
5
|
+
import { IdentityContext } from './identity-context.js'
|
|
6
|
+
import { SystemIdentityContext, useSystemIdentityContext } from './system-identity-context.js'
|
|
7
|
+
|
|
8
|
+
describe('SystemIdentityContext', () => {
|
|
9
|
+
it('isAuthenticated should return true', async () => {
|
|
10
|
+
const ctx = new SystemIdentityContext()
|
|
11
|
+
expect(await ctx.isAuthenticated()).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('isAuthorized should return true without roles', async () => {
|
|
15
|
+
const ctx = new SystemIdentityContext()
|
|
16
|
+
expect(await ctx.isAuthorized()).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('isAuthorized should return true with roles', async () => {
|
|
20
|
+
const ctx = new SystemIdentityContext()
|
|
21
|
+
expect(await ctx.isAuthorized('admin', 'superuser')).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('getCurrentUser should return default system user', async () => {
|
|
25
|
+
const ctx = new SystemIdentityContext()
|
|
26
|
+
const user = await ctx.getCurrentUser()
|
|
27
|
+
expect(user).toEqual({ username: 'system', roles: [] })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('getCurrentUser should respect custom username', async () => {
|
|
31
|
+
const ctx = new SystemIdentityContext({ username: 'migration-job' })
|
|
32
|
+
const user = await ctx.getCurrentUser()
|
|
33
|
+
expect(user).toEqual({ username: 'migration-job', roles: [] })
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('useSystemIdentityContext', () => {
|
|
38
|
+
it('should return a child injector, not the parent', async () => {
|
|
39
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
40
|
+
const child = useSystemIdentityContext({ injector: parent })
|
|
41
|
+
expect(child).not.toBe(parent)
|
|
42
|
+
await child[Symbol.asyncDispose]()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should resolve IdentityContext to a SystemIdentityContext', async () => {
|
|
47
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
48
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
49
|
+
const ctx = child.getInstance(IdentityContext)
|
|
50
|
+
expect(ctx).toBeInstanceOf(SystemIdentityContext)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should be authenticated and authorized via helpers', async () => {
|
|
56
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
57
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
58
|
+
expect(await isAuthenticated(child)).toBe(true)
|
|
59
|
+
expect(await isAuthorized(child, 'admin')).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should return the configured username via getCurrentUser', async () => {
|
|
65
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
66
|
+
await usingAsync(useSystemIdentityContext({ injector: parent, username: 'seed-script' }), async (child) => {
|
|
67
|
+
const user = await getCurrentUser(child)
|
|
68
|
+
expect(user).toEqual({ username: 'seed-script', roles: [] })
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should use default username when not specified', async () => {
|
|
74
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
75
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
76
|
+
const user = await getCurrentUser(child)
|
|
77
|
+
expect(user.username).toBe('system')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should dispose the child injector after usingAsync completes', async () => {
|
|
83
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
84
|
+
let childRef: Injector | undefined
|
|
85
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
86
|
+
childRef = child
|
|
87
|
+
})
|
|
88
|
+
expect(childRef).toBeDefined()
|
|
89
|
+
expect(() => childRef!.getInstance(IdentityContext)).toThrow('Injector already disposed')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should not dispose the parent injector', async () => {
|
|
94
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
95
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async () => {
|
|
96
|
+
// no-op
|
|
97
|
+
})
|
|
98
|
+
expect(() => parent.getInstance(IdentityContext)).not.toThrow()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Injector } from '@furystack/inject'
|
|
2
|
+
|
|
3
|
+
import { IdentityContext } from './identity-context.js'
|
|
4
|
+
import type { User } from './models/user.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An elevated {@link IdentityContext} that is always authenticated and authorized.
|
|
8
|
+
* Intended for trusted server-side operations such as background jobs, migrations, and seed scripts.
|
|
9
|
+
*
|
|
10
|
+
* **Warning:** This context bypasses **all** authorization checks. Never use it in user-facing
|
|
11
|
+
* request pipelines or any context where untrusted input could reach the DataSet.
|
|
12
|
+
* Prefer {@link useSystemIdentityContext} for scoped usage with automatic cleanup.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
17
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
18
|
+
* import { usingAsync } from '@furystack/utils'
|
|
19
|
+
*
|
|
20
|
+
* await usingAsync(
|
|
21
|
+
* useSystemIdentityContext({ injector, username: 'migration-job' }),
|
|
22
|
+
* async (systemInjector) => {
|
|
23
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
24
|
+
* await dataSet.add(systemInjector, newEntity)
|
|
25
|
+
* },
|
|
26
|
+
* )
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class SystemIdentityContext extends IdentityContext {
|
|
30
|
+
private readonly username: string
|
|
31
|
+
|
|
32
|
+
constructor(options?: { username?: string }) {
|
|
33
|
+
super()
|
|
34
|
+
this.username = options?.username ?? 'system'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public override isAuthenticated() {
|
|
38
|
+
return Promise.resolve(true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public override isAuthorized(..._roles: string[]) {
|
|
42
|
+
return Promise.resolve(true)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public override getCurrentUser<TUser extends User>(): Promise<TUser> {
|
|
46
|
+
return Promise.resolve({ username: this.username, roles: [] } as unknown as TUser)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a scoped child injector with an elevated {@link SystemIdentityContext}.
|
|
52
|
+
* The returned injector is {@link AsyncDisposable} and works with `usingAsync()` for automatic cleanup.
|
|
53
|
+
*
|
|
54
|
+
* **Warning:** The returned injector bypasses **all** authorization checks. Only use this in trusted
|
|
55
|
+
* server-side contexts (background jobs, migrations, seed scripts). Never pass the returned injector
|
|
56
|
+
* to user-facing request handlers.
|
|
57
|
+
*
|
|
58
|
+
* @param options.injector The parent injector to create a child from
|
|
59
|
+
* @param options.username The username for the system identity (defaults to `'system'`)
|
|
60
|
+
* @returns A child injector with the SystemIdentityContext set as the IdentityContext
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
65
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
66
|
+
* import { usingAsync } from '@furystack/utils'
|
|
67
|
+
*
|
|
68
|
+
* await usingAsync(
|
|
69
|
+
* useSystemIdentityContext({ injector, username: 'seed-script' }),
|
|
70
|
+
* async (systemInjector) => {
|
|
71
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
72
|
+
* await dataSet.add(systemInjector, { value: 'seeded' })
|
|
73
|
+
* },
|
|
74
|
+
* )
|
|
75
|
+
* // systemInjector is disposed here -- all scoped instances cleaned up
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const useSystemIdentityContext = (options: { injector: Injector; username?: string }): Injector => {
|
|
79
|
+
const ctx = new SystemIdentityContext({ username: options.username })
|
|
80
|
+
const childInjector = options.injector.createChild({ owner: 'SystemIdentityContext' })
|
|
81
|
+
childInjector.setExplicitInstance(ctx, IdentityContext)
|
|
82
|
+
return childInjector
|
|
83
|
+
}
|