@cratis/arc 22.14.2 → 22.15.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.
Files changed (33) hide show
  1. package/dist/cjs/identity/IdentityProvider.js +3 -3
  2. package/dist/cjs/identity/IdentityProvider.js.map +1 -1
  3. package/dist/cjs/identity/deserializeIdentityDetails.js +70 -0
  4. package/dist/cjs/identity/deserializeIdentityDetails.js.map +1 -0
  5. package/dist/cjs/identity/index.js +2 -0
  6. package/dist/cjs/identity/index.js.map +1 -1
  7. package/dist/esm/identity/IdentityProvider.d.ts.map +1 -1
  8. package/dist/esm/identity/IdentityProvider.js +3 -3
  9. package/dist/esm/identity/IdentityProvider.js.map +1 -1
  10. package/dist/esm/identity/deserializeIdentityDetails.d.ts +3 -0
  11. package/dist/esm/identity/deserializeIdentityDetails.d.ts.map +1 -0
  12. package/dist/esm/identity/deserializeIdentityDetails.js +68 -0
  13. package/dist/esm/identity/deserializeIdentityDetails.js.map +1 -0
  14. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_a_details_type_that_declares_no_fields.d.ts +2 -0
  15. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_a_details_type_that_declares_no_fields.d.ts.map +1 -0
  16. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_a_details_type_that_declares_no_fields.js +36 -0
  17. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_a_details_type_that_declares_no_fields.js.map +1 -0
  18. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_type_safe_details.js +18 -4
  19. package/dist/esm/identity/for_IdentityProvider/when_getting_current/with_type_safe_details.js.map +1 -1
  20. package/dist/esm/identity/for_IdentityProvider/when_refreshing/with_type_safe_details.js +18 -4
  21. package/dist/esm/identity/for_IdentityProvider/when_refreshing/with_type_safe_details.js.map +1 -1
  22. package/dist/esm/identity/index.d.ts +1 -0
  23. package/dist/esm/identity/index.d.ts.map +1 -1
  24. package/dist/esm/identity/index.js +1 -0
  25. package/dist/esm/identity/index.js.map +1 -1
  26. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  27. package/identity/IdentityProvider.ts +4 -3
  28. package/identity/deserializeIdentityDetails.ts +79 -0
  29. package/identity/for_IdentityProvider/when_getting_current/with_a_details_type_that_declares_no_fields.ts +48 -0
  30. package/identity/for_IdentityProvider/when_getting_current/with_type_safe_details.ts +17 -8
  31. package/identity/for_IdentityProvider/when_refreshing/with_type_safe_details.ts +16 -7
  32. package/identity/index.ts +1 -0
  33. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { Constructor, JsonSerializer } from '@cratis/fundamentals';
4
+ import { Constructor } from '@cratis/fundamentals';
5
5
  import { IIdentityProvider } from './IIdentityProvider';
6
6
  import { IIdentity } from './IIdentity';
7
7
  import { IdentityProviderResult } from './IdentityProviderResult';
8
+ import { deserializeIdentityDetails } from './deserializeIdentityDetails';
8
9
  import { GetHttpHeaders } from '../GetHttpHeaders';
9
10
  import { Globals } from '../Globals';
10
11
  import { UrlHelpers } from '../UrlHelpers';
@@ -55,7 +56,7 @@ export class IdentityProvider extends IIdentityProvider {
55
56
  if (cookie.length == 2) {
56
57
  const json = atob(cookie[1]);
57
58
  const result = JSON.parse(json) as IdentityProviderResult;
58
- const details = type ? JsonSerializer.deserializeFromInstance(type, result.details) : result.details;
59
+ const details = deserializeIdentityDetails(type, result.details);
59
60
  return {
60
61
  id: result.id,
61
62
  name: result.name,
@@ -93,7 +94,7 @@ export class IdentityProvider extends IIdentityProvider {
93
94
  }
94
95
 
95
96
  const result = await response.json() as IdentityProviderResult;
96
- const details = type ? JsonSerializer.deserializeFromInstance(type, result.details) : result.details;
97
+ const details = deserializeIdentityDetails(type, result.details);
97
98
 
98
99
  return {
99
100
  id: result.id,
@@ -0,0 +1,79 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor, Fields, JsonSerializer } from '@cratis/fundamentals';
5
+
6
+ /*
7
+ * ⚠️ deserializeIdentityDetails is exported deliberately, unlike deserializeQueryModel in
8
+ * ../queries/deserializeQueryModel.ts.
9
+ *
10
+ * JsonSerializer's converter registry is module state, and a consumer package can easily resolve a
11
+ * different physical copy of @cratis/fundamentals than this one - which is why deserializeQueryModel
12
+ * stays package-private. Identity details are different: @cratis/arc.react's useIdentity() needs to
13
+ * apply the exact same deserialization as this package's own IdentityProvider, for a payload that may
14
+ * already be flowing through this package's IdentityProvider/IIdentity. Exporting the function itself
15
+ * - rather than having @cratis/arc.react re-implement the same logic against its own copy of
16
+ * @cratis/fundamentals - guarantees every identity details value is deserialized through this one
17
+ * physical JsonSerializer/Fields, regardless of which package's copy of @cratis/fundamentals the
18
+ * caller resolved.
19
+ */
20
+
21
+ /**
22
+ * Determines whether a details type is a JavaScript primitive wrapper rather than a real,
23
+ * `@field`-decorated model.
24
+ * @param {Constructor} type The details type to check.
25
+ * @returns {boolean} True when the type is a primitive wrapper.
26
+ */
27
+ function isPrimitiveDetailsType(type: Constructor): boolean {
28
+ return type === String || type === Number || type === Boolean;
29
+ }
30
+
31
+ /**
32
+ * Deserializes identity details into their strongly-typed shape, guarding every way that doing so
33
+ * would otherwise crash or silently destroy the payload.
34
+ * @param {Constructor | undefined} type The details type to deserialize into, or `undefined` to leave
35
+ * the payload untouched.
36
+ * @param {unknown} details The raw details payload - parsed JSON from the identity cookie or the
37
+ * `/.cratis/me` endpoint.
38
+ * @returns {unknown} The deserialized instance, or `details` unchanged when deserializing it would be
39
+ * unsafe, pointless, or has already been done.
40
+ * @remarks
41
+ * {@link JsonSerializer.deserializeFromInstance} assumes a well-formed object and a target type
42
+ * carrying `@field` metadata - neither is guaranteed here:
43
+ * - No type means there is nothing to deserialize into.
44
+ * - A `null`/`undefined`/non-object payload would make `deserializeFromInstance`'s internal
45
+ * `instance[field.name]` lookup throw.
46
+ * - A payload that is already an instance of the target type has already been deserialized -
47
+ * deserializing it again is destructive, not merely wasteful: it can throw on a nested temporal
48
+ * value and double-wraps a concept (`{ value: { value: '...' } }`).
49
+ * - `Object` and the primitive wrapper types (`String`, `Number`, `Boolean`) are not real,
50
+ * `@field`-decorated models; deserializing into them would discard the payload.
51
+ * - A type with no `@field`-decorated members deserializes to an empty instance, silently discarding
52
+ * the payload. That is worse than leaving the payload alone, so this warns and passes it through
53
+ * instead - preserving data beats a hard crash for anyone upgrading with an undecorated type.
54
+ */
55
+ export function deserializeIdentityDetails(type: Constructor | undefined, details: unknown): unknown {
56
+ if (!type || type === Object || isPrimitiveDetailsType(type)) {
57
+ return details;
58
+ }
59
+
60
+ if (details === null || details === undefined || typeof details !== 'object') {
61
+ return details;
62
+ }
63
+
64
+ if (details instanceof type) {
65
+ return details;
66
+ }
67
+
68
+ const fields = Fields.getFieldsForType(type);
69
+ if (fields.length === 0) {
70
+ console.warn(
71
+ `Identity details type '${type.name}' has no @field-decorated members. Deserializing into ` +
72
+ 'it would discard the payload and return an empty instance, so the raw payload is being ' +
73
+ 'passed through instead. Add @field decorators for every property that should be ' +
74
+ 'populated, or omit the details type to receive the raw payload as-is.');
75
+ return details;
76
+ }
77
+
78
+ return JsonSerializer.deserializeFromInstance(type as Constructor<object>, details);
79
+ }
@@ -0,0 +1,48 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { IdentityProvider } from '../../IdentityProvider';
5
+ import { an_identity_provider } from '../given/an_identity_provider';
6
+ import { given } from '../../../given';
7
+
8
+ // Deliberately undecorated - no @field members. Deserializing into this would construct
9
+ // `new UndecoratedDetails()` and copy nothing, discarding whatever the server sent.
10
+ class UndecoratedDetails {
11
+ userId!: string;
12
+ role!: string;
13
+ }
14
+
15
+ describe('when getting current with a details type that declares no fields', given(an_identity_provider, () => {
16
+ let originalConsoleWarn: typeof console.warn;
17
+ let result: { details: UndecoratedDetails };
18
+
19
+ beforeEach(async () => {
20
+ originalConsoleWarn = console.warn;
21
+ console.warn = () => { /* Suppressed during test */ };
22
+
23
+ const identityData = {
24
+ id: 'test-user-id',
25
+ name: 'Test User',
26
+ details: {
27
+ userId: 'u-1',
28
+ role: 'admin'
29
+ }
30
+ };
31
+ const encodedData = btoa(JSON.stringify(identityData));
32
+ (global as { document?: { cookie: string } }).document!.cookie = `.cratis-identity=${encodedData}`;
33
+
34
+ const identity = await IdentityProvider.getCurrent(UndecoratedDetails);
35
+ result = { details: identity.details };
36
+ });
37
+
38
+ afterEach(() => { console.warn = originalConsoleWarn; });
39
+
40
+ it('should hand back the payload untouched', () => {
41
+ (result.details as unknown as { userId: string }).userId.should.equal('u-1');
42
+ (result.details as unknown as { role: string }).role.should.equal('admin');
43
+ });
44
+
45
+ it('should not blank the details', () => {
46
+ result.details.should.not.be.instanceOf(UndecoratedDetails);
47
+ });
48
+ }));
@@ -1,29 +1,33 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { Guid } from '@cratis/fundamentals';
4
+ import { field, Guid } from '@cratis/fundamentals';
5
5
  import { IdentityProvider } from '../../IdentityProvider';
6
6
  import { an_identity_provider } from '../given/an_identity_provider';
7
7
  import { given } from '../../../given';
8
8
 
9
9
  class TestDetails {
10
- userId: Guid = Guid.empty;
11
- role: string = '';
10
+ @field(Guid)
11
+ userId!: Guid;
12
+
13
+ @field(String)
14
+ role!: string;
12
15
  }
13
16
 
14
17
  describe('when getting current with type safe details', given(an_identity_provider, () => {
15
18
  let result: { id: string; name: string; details: TestDetails };
19
+ let testGuid: Guid;
16
20
 
17
21
  beforeEach(async () => {
18
- const testGuid = Guid.create();
19
-
22
+ testGuid = Guid.create();
23
+
20
24
  // Mock document.cookie with base64-encoded identity data
21
25
  const identityData = {
22
26
  id: 'test-user-id',
23
27
  name: 'Test User',
24
- details: {
25
- userId: testGuid.toString(),
26
- role: 'admin'
28
+ details: {
29
+ userId: testGuid.toString(),
30
+ role: 'admin'
27
31
  }
28
32
  };
29
33
  const encodedData = btoa(JSON.stringify(identityData));
@@ -40,4 +44,9 @@ describe('when getting current with type safe details', given(an_identity_provid
40
44
  it('should deserialize details with proper types', () => {
41
45
  result.details.userId.should.be.instanceOf(Guid);
42
46
  });
47
+
48
+ it('should carry over the value the server sent', () => {
49
+ result.details.userId.toString().should.equal(testGuid.toString());
50
+ result.details.role.should.equal('admin');
51
+ });
43
52
  }));
@@ -1,29 +1,33 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { Guid } from '@cratis/fundamentals';
4
+ import { field, Guid } from '@cratis/fundamentals';
5
5
  import { IdentityProvider } from '../../IdentityProvider';
6
6
  import { an_identity_provider } from '../given/an_identity_provider';
7
7
  import { given } from '../../../given';
8
8
 
9
9
  class TestDetails {
10
- userId: Guid = Guid.empty;
11
- role: string = '';
10
+ @field(Guid)
11
+ userId!: Guid;
12
+
13
+ @field(String)
14
+ role!: string;
12
15
  }
13
16
 
14
17
  describe('when refreshing with type safe details', given(an_identity_provider, context => {
15
18
  let result: { id: string; name: string; details: TestDetails };
19
+ let testGuid: Guid;
16
20
 
17
21
  beforeEach(async () => {
18
- const testGuid = Guid.create();
22
+ testGuid = Guid.create();
19
23
  context.fetchStub.resolves({
20
24
  ok: true,
21
25
  json: async () => ({
22
26
  id: 'test-user-id',
23
27
  name: 'Test User',
24
- details: {
25
- userId: testGuid.toString(),
26
- role: 'admin'
28
+ details: {
29
+ userId: testGuid.toString(),
30
+ role: 'admin'
27
31
  }
28
32
  })
29
33
  } as Response);
@@ -39,4 +43,9 @@ describe('when refreshing with type safe details', given(an_identity_provider, c
39
43
  it('should deserialize details with proper types', () => {
40
44
  result.details.userId.should.be.instanceOf(Guid);
41
45
  });
46
+
47
+ it('should carry over the value the server sent', () => {
48
+ result.details.userId.toString().should.equal(testGuid.toString());
49
+ result.details.role.should.equal('admin');
50
+ });
42
51
  }));
package/identity/index.ts CHANGED
@@ -5,3 +5,4 @@ export * from './IdentityProvider';
5
5
  export * from './IdentityProviderResult';
6
6
  export * from './IIdentityProvider';
7
7
  export * from './IIdentity';
8
+ export * from './deserializeIdentityDetails';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/arc",
3
- "version": "22.14.2",
3
+ "version": "22.15.0",
4
4
  "description": "TypeScript command, query, validation, identity, and messaging runtime for Cratis Arc — the CQRS application framework for ASP.NET Core — used by generated proxies.",
5
5
  "keywords": [
6
6
  "cratis",