@owlmeans/auth-common 0.1.18-rc.41 → 0.1.18-rc.43

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/README.md CHANGED
@@ -12,7 +12,7 @@ Predefined authentication protocols, guards, and constants shared between server
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/auth-common@^0.1.18-rc.41
15
+ bun add @owlmeans/auth-common@^0.1.18-rc.43
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -98,7 +98,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
98
98
  your project's skill store (`.agents/skills/`):
99
99
 
100
100
  ```sh
101
- npx @owlmeans/agent-skills@^0.1.18-rc.42
101
+ npx @owlmeans/agent-skills@^0.1.18-rc.45
102
102
  ```
103
103
 
104
104
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/auth-common",
4
- "version": "0.1.18-rc.41",
5
- "generatedAt": "2026-09-29T08:01:58.121Z",
4
+ "version": "0.1.18-rc.43",
5
+ "generatedAt": "2026-10-01T23:27:48.728Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/auth-common
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/auth-common": "^0.1.18-rc.41"` in `dependencies`
11
+ **Install:** `"@owlmeans/auth-common": "^0.1.18-rc.43"` in `dependencies`
12
12
 
13
13
  Everything a server and a browser must agree on to talk authentication: aliases, the shared
14
14
  protocol declarations, the signature guard, and the contract for resolving the organization
@@ -83,7 +83,7 @@ import { attachEntity, entityKeyOf, requireEntityKey, requireEntity } from '@owl
83
83
 
84
84
  | Helper | What it answers |
85
85
  |--------|-----------------|
86
- | `attachEntity(context, request)` | Resolve the slug on `request.auth` and set `request.entity`, canonicalizing a retired slug to the current one. A no-op when no resolver is registered; throws `AuthenFailed('entity')` when the token names an organization that will not resolve |
86
+ | `attachEntity(context, request)` | Resolve the slug on `request.auth` and set `request.entity`, canonicalizing a retired slug to the current one. Keeps an entity the guard already attached when its `slug` equals the token's exactly, and drops one that does not. Otherwise a no-op when no resolver is registered; throws `AuthenFailed('entity')` when the token names an organization that will not resolve |
87
87
  | `entityKeyOf(req)` | The value to store and query organization-scoped records by — `req.entity?.id`, falling back to the token's slug where no resolver exists |
88
88
  | `requireEntityKey(req)` | Same, throwing `AuthorizationError` when the request carries no organization |
89
89
  | `requireEntity(req)` | The full `ResolvedEntity` (`id`, `slug`, `iamKey`), throwing `AuthorizationError` when nothing resolved |
@@ -93,6 +93,12 @@ any socket that authenticates after its connection is already open. A path that
93
93
  without it leaves `request.entity` empty and its handlers silently compare a slug against stored
94
94
  ids.
95
95
 
96
+ A guard may attach the entity itself when its authority names the organization — the OIDC guard
97
+ does for a session of a tenanted client (`@owlmeans/oidc`), whose relying party has no registry to
98
+ resolve from. `attachEntity` trusts that attachment only while its `slug` is the token's
99
+ `entitySlugOf(request.auth)`; on any mismatch it removes it and resolves as if nothing had been
100
+ attached, so a stale or foreign entity can never reach a handler.
101
+
96
102
  Never build a user-facing name (a hostname, a display label) from `entityKeyOf`. Those want the
97
103
  current slug, `req.entity?.slug`.
98
104
 
package/build/entity.d.ts CHANGED
@@ -34,8 +34,14 @@ export declare const requireEntity: (req: AbstractRequest) => ResolvedEntity;
34
34
  * its handlers quietly fall back to comparing a slug against stored ids — which surfaces as "this
35
35
  * project does not exist" rather than as a missing resolution, and costs an afternoon to find.
36
36
  *
37
- * A no-op when no resolver is registered: such a deployment has no organization store, and the
38
- * slug is the only identifier it has.
37
+ * An entity the guard already attached is kept while its slug is exactly the token's: a guard
38
+ * whose authority names the organization itself (an OIDC session of a tenanted client) is the only
39
+ * source such a deployment has, because the registry that owns the organization is not its own.
40
+ * One whose slug differs is never trusted — it is dropped, and resolution proceeds as if it had
41
+ * not been there.
42
+ *
43
+ * Otherwise a no-op when no resolver is registered: such a deployment has no organization store,
44
+ * and the slug is the only identifier it has.
39
45
  *
40
46
  * @throws {AuthenFailed} when the token names an organization that cannot be resolved.
41
47
  */
@@ -1 +1 @@
1
- {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,QAAS,eAAe,KAAG,MAAM,GAAG,SAClB,CAAA;AAE1C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAS,eAAe,KAAG,MAOvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAS,eAAe,KAAG,cAMpD,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,YACd,YAAY,CAAC,GAAG,CAAC,WAAW,eAAe,KACnD,OAAO,CAAC,cAAc,GAAG,SAAS,CAiBpC,CAAA"}
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,QAAS,eAAe,KAAG,MAAM,GAAG,SAClB,CAAA;AAE1C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAS,eAAe,KAAG,MAOvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAS,eAAe,KAAG,cAMpD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY,YACd,YAAY,CAAC,GAAG,CAAC,WAAW,eAAe,KACnD,OAAO,CAAC,cAAc,GAAG,SAAS,CAwBpC,CAAA"}
package/build/entity.js CHANGED
@@ -45,13 +45,25 @@ export const requireEntity = (req) => {
45
45
  * its handlers quietly fall back to comparing a slug against stored ids — which surfaces as "this
46
46
  * project does not exist" rather than as a missing resolution, and costs an afternoon to find.
47
47
  *
48
- * A no-op when no resolver is registered: such a deployment has no organization store, and the
49
- * slug is the only identifier it has.
48
+ * An entity the guard already attached is kept while its slug is exactly the token's: a guard
49
+ * whose authority names the organization itself (an OIDC session of a tenanted client) is the only
50
+ * source such a deployment has, because the registry that owns the organization is not its own.
51
+ * One whose slug differs is never trusted — it is dropped, and resolution proceeds as if it had
52
+ * not been there.
53
+ *
54
+ * Otherwise a no-op when no resolver is registered: such a deployment has no organization store,
55
+ * and the slug is the only identifier it has.
50
56
  *
51
57
  * @throws {AuthenFailed} when the token names an organization that cannot be resolved.
52
58
  */
53
59
  export const attachEntity = async (context, request) => {
54
60
  const slug = entitySlugOf(request.auth);
61
+ if (request.entity != null) {
62
+ if (slug != null && request.entity.slug === slug) {
63
+ return request.entity;
64
+ }
65
+ request.entity = undefined;
66
+ }
55
67
  if (slug == null || !context.hasService(ENTITY_RESOLVER)) {
56
68
  return undefined;
57
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE/E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAoB,EAAsB,EAAE,CACtE,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAoB,EAAU,EAAE;IAC/D,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC9B,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAoB,EAAkB,EAAE;IACpE,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAA;AACnB,CAAC,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,OAA0B,EAAE,OAAwB,EACf,EAAE;IACvC,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACzD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAwB,eAAe,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1F,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAA;IAClC,CAAC;IAED,gGAAgG;IAChG,qFAAqF;IACrF,OAAO,CAAC,IAAK,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAA;IACtC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAA;IAEvB,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
1
+ {"version":3,"file":"entity.js","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE/E,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAoB,EAAsB,EAAE,CACtE,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAoB,EAAU,EAAE;IAC/D,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC9B,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAoB,EAAkB,EAAE;IACpE,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IAED,OAAO,GAAG,CAAC,MAAM,CAAA;AACnB,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,OAA0B,EAAE,OAAwB,EACf,EAAE;IACvC,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACjD,OAAO,OAAO,CAAC,MAAM,CAAA;QACvB,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACzD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAwB,eAAe,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1F,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAA;IAClC,CAAC;IAED,gGAAgG;IAChG,qFAAqF;IACrF,OAAO,CAAC,IAAK,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAA;IACtC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAA;IAEvB,OAAO,MAAM,CAAA;AACf,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/auth-common",
3
- "version": "0.1.18-rc.41",
3
+ "version": "0.1.18-rc.43",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -29,19 +29,19 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@owlmeans/auth": "^0.1.18-rc.36",
33
- "@owlmeans/auth-common": "^0.1.18-rc.41",
32
+ "@owlmeans/auth": "^0.1.18-rc.37",
33
+ "@owlmeans/auth-common": "^0.1.18-rc.43",
34
34
  "@owlmeans/basic-ids": "^0.1.18-rc.34",
35
- "@owlmeans/basic-keys": "^0.1.18-rc.42",
36
- "@owlmeans/client-entrypoint": "^0.1.18-rc.41",
35
+ "@owlmeans/basic-keys": "^0.1.18-rc.44",
36
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.43",
37
37
  "@owlmeans/context": "^0.1.18-rc.32",
38
- "@owlmeans/entrypoint": "^0.1.18-rc.37",
38
+ "@owlmeans/entrypoint": "^0.1.18-rc.38",
39
39
  "@owlmeans/resource": "^0.1.18-rc.37",
40
40
  "@owlmeans/route": "^0.1.18-rc.33"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@owlmeans/dep-config": "workspace:*",
44
- "@owlmeans/test-auth": "^0.1.18-rc.40",
44
+ "@owlmeans/test-auth": "^0.1.18-rc.42",
45
45
  "@types/bun": "^1.4.0",
46
46
  "nodemon": "^3.1.14",
47
47
  "typescript": "^7.0.2"
package/src/entity.ts CHANGED
@@ -55,8 +55,14 @@ export const requireEntity = (req: AbstractRequest): ResolvedEntity => {
55
55
  * its handlers quietly fall back to comparing a slug against stored ids — which surfaces as "this
56
56
  * project does not exist" rather than as a missing resolution, and costs an afternoon to find.
57
57
  *
58
- * A no-op when no resolver is registered: such a deployment has no organization store, and the
59
- * slug is the only identifier it has.
58
+ * An entity the guard already attached is kept while its slug is exactly the token's: a guard
59
+ * whose authority names the organization itself (an OIDC session of a tenanted client) is the only
60
+ * source such a deployment has, because the registry that owns the organization is not its own.
61
+ * One whose slug differs is never trusted — it is dropped, and resolution proceeds as if it had
62
+ * not been there.
63
+ *
64
+ * Otherwise a no-op when no resolver is registered: such a deployment has no organization store,
65
+ * and the slug is the only identifier it has.
60
66
  *
61
67
  * @throws {AuthenFailed} when the token names an organization that cannot be resolved.
62
68
  */
@@ -64,6 +70,13 @@ export const attachEntity = async (
64
70
  context: BasicContext<any>, request: AbstractRequest
65
71
  ): Promise<ResolvedEntity | undefined> => {
66
72
  const slug = entitySlugOf(request.auth)
73
+ if (request.entity != null) {
74
+ if (slug != null && request.entity.slug === slug) {
75
+ return request.entity
76
+ }
77
+ request.entity = undefined
78
+ }
79
+
67
80
  if (slug == null || !context.hasService(ENTITY_RESOLVER)) {
68
81
  return undefined
69
82
  }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { AppType, createService, makeBasicContext } from '@owlmeans/context'
3
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
4
+ import { AuthenFailed, AuthRole } from '@owlmeans/auth'
5
+ import type { Auth } from '@owlmeans/auth'
6
+ import type { AbstractRequest, ResolvedEntity } from '@owlmeans/entrypoint'
7
+ import { attachEntity } from '../src/entity.js'
8
+ import { ENTITY_RESOLVER } from '../src/consts.js'
9
+ import type { EntityResolverService, OrgEntityRef } from '../src/types.js'
10
+
11
+ const ACME: OrgEntityRef = { id: '0123456789abcdef01234567', slug: 'acme', formerSlugs: ['acme-old'], iamKey: 'acme-key' }
12
+
13
+ /** A context with an in-memory registry of one organization, or none at all. */
14
+ const contextWith = async (registry?: OrgEntityRef[]) => {
15
+ const ctx = makeBasicContext<BasicConfig>({ ready: false, service: 'entity-tests', type: AppType.Backend })
16
+ const calls: string[] = []
17
+ if (registry != null) {
18
+ ctx.registerService(createService<EntityResolverService>(ENTITY_RESOLVER, {
19
+ resolve: async value => {
20
+ calls.push(value)
21
+ return registry.find(entity => entity.id === value || entity.slug === value
22
+ || entity.formerSlugs?.includes(value) || entity.iamKey === value) ?? null
23
+ },
24
+ byId: async id => registry.find(entity => entity.id === id) ?? null,
25
+ mintSlug: async () => 'minted',
26
+ rename: async () => { throw new Error('not used') },
27
+ mintName: async () => { throw new Error('not used') },
28
+ }))
29
+ }
30
+ ctx.configure()
31
+ await ctx.init()
32
+
33
+ return { ctx: ctx as BasicContext<BasicConfig>, calls }
34
+ }
35
+
36
+ const requestFor = (entitySlug?: string, entity?: ResolvedEntity): AbstractRequest => ({
37
+ alias: 'any', path: '/', params: {}, query: {}, headers: {},
38
+ auth: { token: 't', type: 'oidc', userId: 'u', role: AuthRole.User, scopes: [], isUser: true,
39
+ createdAt: new Date(), ...(entitySlug != null ? { entitySlug } : {}) } as Auth,
40
+ ...(entity != null ? { entity } : {}),
41
+ })
42
+
43
+ const attached: ResolvedEntity = { id: 'iam-key-tenant', slug: 'tenant', iamKey: 'iam-key-tenant' }
44
+
45
+ describe('attachEntity — an entity the guard attached', () => {
46
+ test('is kept when its slug is the token one, with no resolver registered', async () => {
47
+ const { ctx } = await contextWith()
48
+ const request = requestFor('tenant', attached)
49
+
50
+ expect(await attachEntity(ctx, request)).toEqual(attached)
51
+ expect(request.entity).toEqual(attached)
52
+ })
53
+
54
+ test('is kept without asking a registered resolver', async () => {
55
+ const { ctx, calls } = await contextWith([ACME])
56
+ const request = requestFor('tenant', attached)
57
+
58
+ expect(await attachEntity(ctx, request)).toEqual(attached)
59
+ expect(calls).toEqual([])
60
+ })
61
+
62
+ test('with another slug falls through to the resolver', async () => {
63
+ const { ctx, calls } = await contextWith([ACME])
64
+ const request = requestFor('acme', attached)
65
+
66
+ expect(await attachEntity(ctx, request)).toEqual(ACME)
67
+ expect(request.entity).toEqual(ACME)
68
+ expect(calls).toEqual(['acme'])
69
+ })
70
+
71
+ test('with another slug and no resolver is dropped, never trusted', async () => {
72
+ const { ctx } = await contextWith()
73
+ const request = requestFor('acme', attached)
74
+
75
+ expect(await attachEntity(ctx, request)).toBeUndefined()
76
+ expect(request.entity).toBeUndefined()
77
+ })
78
+
79
+ test('on a token naming no organization is dropped', async () => {
80
+ const { ctx } = await contextWith()
81
+ const request = requestFor(undefined, attached)
82
+
83
+ expect(await attachEntity(ctx, request)).toBeUndefined()
84
+ expect(request.entity).toBeUndefined()
85
+ })
86
+ })
87
+
88
+ describe('attachEntity — the resolver path', () => {
89
+ test('resolves and canonicalizes a retired slug', async () => {
90
+ const { ctx } = await contextWith([ACME])
91
+ const request = requestFor('acme-old')
92
+
93
+ expect(await attachEntity(ctx, request)).toEqual(ACME)
94
+ expect(request.entity).toEqual(ACME)
95
+ expect(request.auth?.entitySlug).toBe('acme')
96
+ })
97
+
98
+ test('refuses a token naming an organization the registry does not know', async () => {
99
+ const { ctx } = await contextWith([ACME])
100
+
101
+ await expect(attachEntity(ctx, requestFor('nobody'))).rejects.toBeInstanceOf(AuthenFailed)
102
+ })
103
+
104
+ test('is a no-op with no resolver and nothing attached', async () => {
105
+ const { ctx } = await contextWith()
106
+ const request = requestFor('acme')
107
+
108
+ expect(await attachEntity(ctx, request)).toBeUndefined()
109
+ expect(request.entity).toBeUndefined()
110
+ expect(request.auth?.entitySlug).toBe('acme')
111
+ })
112
+ })