@furystack/core 15.0.36 → 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 CHANGED
@@ -1,5 +1,41 @@
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
+
3
39
  ## [15.0.36] - 2026-02-11
4
40
 
5
41
  ### ⬆️ Dependencies
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
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=system-identity-context.spec.d.ts.map
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AAE9F,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QAClD,MAAM,GAAG,GAAG,IAAI,qBAAqB,EAAE,CAAA;QACvC,MAAM,CAAC,MAAM,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,GAAG,GAAG,IAAI,qBAAqB,EAAE,CAAA;QACvC,MAAM,CAAC,MAAM,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,GAAG,GAAG,IAAI,qBAAqB,EAAE,CAAA;QACvC,MAAM,CAAC,MAAM,GAAG,CAAC,YAAY,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,GAAG,GAAG,IAAI,qBAAqB,EAAE,CAAA;QACvC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,cAAc,EAAE,CAAA;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,GAAG,GAAG,IAAI,qBAAqB,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,CAAA;QACpE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,cAAc,EAAE,CAAA;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,KAAK,GAAG,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;YAC5D,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC9B,MAAM,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;QACpC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/E,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,eAAe,CAAC,CAAA;gBAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAA;YACnD,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/E,MAAM,CAAC,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAC/C,MAAM,CAAC,MAAM,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACvD,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBACxG,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,CAAA;gBACxC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;YAC9D,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/E,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,CAAA;gBACxC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACtC,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,IAAI,QAA8B,CAAA;YAClC,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/E,QAAQ,GAAG,KAAK,CAAA;YAClB,CAAC,CAAC,CAAA;YACF,MAAM,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAA;YAC9B,MAAM,CAAC,GAAG,EAAE,CAAC,QAAS,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAA;QAC3F,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,UAAU,CAAC,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChD,MAAM,UAAU,CAAC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE;gBAC1E,QAAQ;YACV,CAAC,CAAC,CAAA;YACF,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QACjE,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furystack/core",
3
- "version": "15.0.36",
3
+ "version": "15.1.0",
4
4
  "description": "Core FuryStack package",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "homepage": "https://github.com/furystack/furystack",
48
48
  "devDependencies": {
49
- "@types/node": "^25.2.3",
49
+ "@types/node": "^25.3.0",
50
50
  "typescript": "^5.9.3",
51
51
  "vitest": "^4.0.18"
52
52
  },
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ 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'
@@ -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
+ }