@owlmeans/planning-auth 0.0.0-stage → 0.1.0-rc.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/README.md +36 -2
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/planning-auth/SKILL.md +40 -0
- package/build/adapter.d.ts +4 -0
- package/build/adapter.d.ts.map +1 -0
- package/build/adapter.js +68 -0
- package/build/adapter.js.map +1 -0
- package/build/consts.d.ts +4 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +4 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/plugin.d.ts +5 -0
- package/build/plugin.d.ts.map +1 -0
- package/build/plugin.js +31 -0
- package/build/plugin.js.map +1 -0
- package/build/types.d.ts +33 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +32 -4
- package/src/adapter.ts +58 -0
- package/src/consts.ts +3 -0
- package/src/index.ts +5 -0
- package/src/plugin.ts +30 -0
- package/src/types.ts +33 -0
- package/tests/adapter.spec.ts +44 -0
- package/tsconfig.json +5 -0
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @owlmeans/planning-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional authentication identity and group links for OwlMeans planning.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun add @owlmeans/planning-auth@^0.1.0-rc.0
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Register `makePlanningAuthPlugin()` for the human/non-human schemas. On a trusted server,
|
|
10
|
+
`attributeCreates: true` fills missing reporters from verified creation context; an optional
|
|
11
|
+
`defaultAssignee` supplies the responsible participant. Clients register the schema-only plugin.
|
|
12
|
+
|
|
13
|
+
`makePlanningAuth({ planning: () => scopedFacade, provider })` exposes `assigneeFor`, `teamFor` and
|
|
14
|
+
`scopeFor`. Stable provider/subject identity maps to a planning id; a nickname collision never
|
|
15
|
+
retargets an existing participant. Teams carry provider-qualified external group ids. Membership
|
|
16
|
+
and permission grants remain the authentication host's responsibility.
|
|
17
|
+
|
|
18
|
+
Planning links always contain planning ids. Human authentication is required by the default schema;
|
|
19
|
+
non-human authentication is optional. Retirement preserves historical attribution.
|
|
20
|
+
|
|
21
|
+
Agent guidance ships in this package's `agent-meta` directory.
|
|
22
|
+
|
|
23
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
24
|
+
## Agent guidance
|
|
25
|
+
|
|
26
|
+
This package ships embedded agent skills under `agent-meta/`. After installing your
|
|
27
|
+
`@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
|
|
28
|
+
your project's skill store (`.agents/skills/`):
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.53
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
35
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
36
|
+
open a PR against the source monorepo.
|
|
37
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"package": "@owlmeans/planning-auth",
|
|
4
|
+
"version": "0.1.0-rc.0",
|
|
5
|
+
"generatedAt": "2026-10-09T11:31:12.872Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "skill",
|
|
10
|
+
"name": "planning-auth",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "skills/planning-auth/SKILL.md",
|
|
13
|
+
"canonicalPath": ".agents/skills/planning-auth/SKILL.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planning-auth
|
|
3
|
+
description: Optional OwlMeans authentication integration for generic planning assignees, creation attribution and external group links. Use when linking authenticated profiles or groups to planning without coupling planning contracts to an authentication provider.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# @owlmeans/planning-auth
|
|
9
|
+
|
|
10
|
+
**Install:** `"@owlmeans/planning-auth": "^0.1.0-rc.0"`. Planning itself remains provider-neutral.
|
|
11
|
+
Register `makePlanningAuthPlugin()` for human/non-human assignee schemas; the default plugin is
|
|
12
|
+
schema-only and safe for clients. Server hosts may set `attributeCreates: true`, `owns` and a
|
|
13
|
+
`defaultAssignee` to fill missing top-level reporter/responsible fields from trusted creation context.
|
|
14
|
+
|
|
15
|
+
`makePlanningAuth({ planning: () => trustedScopedFacade, provider?, humanType?, nonHumanType?,
|
|
16
|
+
defaultAssignee? })` creates one context-bound adapter. `assigneeFor({ externalId, nickname,
|
|
17
|
+
kind?, type?, fields? })` resolves the stable provider/subject link within the organization entity;
|
|
18
|
+
it never identifies a user by display name. Nickname collisions create a distinct nickname rather
|
|
19
|
+
than repointing an existing identity. Concurrent creation rereads the stable identity on conflict.
|
|
20
|
+
|
|
21
|
+
`scopeFor(scope)` resolves verified profileId/userId and returns `assigneeId`, with optional
|
|
22
|
+
`defaultAssigneeId`. Use only trusted authenticated scope, never caller-supplied organization ids
|
|
23
|
+
or creator claims. `teamFor({ externalId, name })` resolves a provider-qualified external group
|
|
24
|
+
link into a reusable planning team. It neither writes permission grants nor synchronizes group
|
|
25
|
+
memberships. Hosts authorize attachment and membership through their existing access resolver.
|
|
26
|
+
|
|
27
|
+
Humans require authentication unless their assignee type explicitly allows omission. Non-human
|
|
28
|
+
assignees may be authenticated but do not require it. Keep all relationship references as planning
|
|
29
|
+
ids; external identity ids remain integration metadata. Retirement preserves historic attribution.
|
|
30
|
+
|
|
31
|
+
In viable, `makeViablePlanningAuthPlugin` owns attribution and its stable internal agent identity;
|
|
32
|
+
compatibility backfill verifies old creator membership before assigning reporters. Generated targets
|
|
33
|
+
use the same adapter when authenticated profile/group mapping is part of their product.
|
|
34
|
+
|
|
35
|
+
## External docs
|
|
36
|
+
|
|
37
|
+
- https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability — issuer plus subject is stable
|
|
38
|
+
identity; names and nicknames are unsuitable as authentication keys.
|
|
39
|
+
- https://www.rfc-editor.org/rfc/rfc7643.html#section-3.1 — externalId is supplied by the provisioning
|
|
40
|
+
client and scoped to the provisioning domain; retain provider context.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PlanningAuth, PlanningAuthOptions } from './types.js';
|
|
2
|
+
/** Authentication integration is an optional planning adapter; planning itself knows no user system. */
|
|
3
|
+
export declare const makePlanningAuth: (opts: PlanningAuthOptions) => PlanningAuth;
|
|
4
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAwB,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEzF,wGAAwG;AACxG,eAAO,MAAM,gBAAgB,SAAU,mBAAmB,KAAG,YAkD5D,CAAA"}
|
package/build/adapter.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { makePlanningAuthPlugin } from './plugin.js';
|
|
2
|
+
import { AssigneeKind, FieldsInvalid, mentionHelper, WorkcardConflict } from '@owlmeans/planning';
|
|
3
|
+
import { OWLMEANS_PLANNING_AUTH_PROVIDER, PLANNING_AUTH_HUMAN_TYPE, PLANNING_AUTH_NON_HUMAN_TYPE } from './consts.js';
|
|
4
|
+
/** Authentication integration is an optional planning adapter; planning itself knows no user system. */
|
|
5
|
+
export const makePlanningAuth = (opts) => {
|
|
6
|
+
const provider = opts.provider ?? OWLMEANS_PLANNING_AUTH_PROVIDER;
|
|
7
|
+
const humanType = opts.humanType ?? PLANNING_AUTH_HUMAN_TYPE;
|
|
8
|
+
const nonHumanType = opts.nonHumanType ?? PLANNING_AUTH_NON_HUMAN_TYPE;
|
|
9
|
+
const assigneeFor = async (identity) => {
|
|
10
|
+
if (identity.externalId.trim() === '')
|
|
11
|
+
throw new FieldsInvalid('auth:external-id-required');
|
|
12
|
+
const authentication = { provider, externalId: identity.externalId };
|
|
13
|
+
const planning = opts.planning();
|
|
14
|
+
const existing = async () => (await planning.assignees.list({ authentication, size: 1 })).items[0];
|
|
15
|
+
const found = await existing();
|
|
16
|
+
if (found != null)
|
|
17
|
+
return found;
|
|
18
|
+
const kind = identity.kind ?? AssigneeKind.Human;
|
|
19
|
+
let nickname = identity.nickname.trim();
|
|
20
|
+
if (mentionHelper.nicknameKey(nickname) === '')
|
|
21
|
+
throw new FieldsInvalid('auth:nickname-required');
|
|
22
|
+
// A nickname collision must never link an existing person to a different authentication subject.
|
|
23
|
+
if ((await planning.assignees.list({ nickname, size: 1 })).total > 0) {
|
|
24
|
+
nickname = `${nickname.slice(0, 64)}-${encodeURIComponent(identity.externalId).slice(-48)}`;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await planning.assignees.create({ nickname, kind, type: identity.type ?? (kind === AssigneeKind.Human ? humanType : nonHumanType), fields: identity.fields ?? {}, authentication });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!(error instanceof WorkcardConflict))
|
|
31
|
+
throw error;
|
|
32
|
+
const raced = await existing();
|
|
33
|
+
if (raced == null)
|
|
34
|
+
throw error;
|
|
35
|
+
return raced;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const adapter = {
|
|
39
|
+
assigneeFor,
|
|
40
|
+
teamFor: async (group) => {
|
|
41
|
+
const planning = opts.planning();
|
|
42
|
+
const externalId = `${provider}:${group.externalId}`;
|
|
43
|
+
const found = (await planning.teams.list({ externalId, size: 1 })).items[0];
|
|
44
|
+
if (found != null)
|
|
45
|
+
return found;
|
|
46
|
+
try {
|
|
47
|
+
return await planning.teams.create({ name: group.name, externalId });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!(error instanceof WorkcardConflict))
|
|
51
|
+
throw error;
|
|
52
|
+
const raced = (await planning.teams.list({ externalId, size: 1 })).items[0];
|
|
53
|
+
if (raced == null)
|
|
54
|
+
throw error;
|
|
55
|
+
return raced;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
scopeFor: async (scope) => {
|
|
59
|
+
const externalId = scope.profileId ?? scope.userId ?? scope.actor?.profileId ?? scope.actor?.userId;
|
|
60
|
+
const reporter = externalId == null ? undefined : await assigneeFor({ externalId, nickname: `user-${externalId}`, kind: AssigneeKind.Human });
|
|
61
|
+
const assignee = opts.defaultAssignee == null ? undefined : await assigneeFor({ ...opts.defaultAssignee, kind: opts.defaultAssignee.kind ?? AssigneeKind.NonHuman });
|
|
62
|
+
return { ...scope, ...(reporter?.id != null ? { assigneeId: reporter.id } : {}), ...(assignee?.id != null ? { defaultAssigneeId: assignee.id } : {}) };
|
|
63
|
+
},
|
|
64
|
+
plugin: makePlanningAuthPlugin(opts),
|
|
65
|
+
};
|
|
66
|
+
return adapter;
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEjG,OAAO,EAAE,+BAA+B,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAGrH,wGAAwG;AACxG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAyB,EAAgB,EAAE;IAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,+BAA+B,CAAA;IACjE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,wBAAwB,CAAA;IAC5D,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,4BAA4B,CAAA;IACtE,MAAM,WAAW,GAAG,KAAK,EAAE,QAA8B,EAAqB,EAAE;QAC9E,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,MAAM,IAAI,aAAa,CAAC,2BAA2B,CAAC,CAAA;QAC3F,MAAM,cAAc,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAA;QACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QAChC,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAClG,MAAM,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAA;QAC9B,IAAI,KAAK,IAAI,IAAI;YAAE,OAAO,KAAK,CAAA;QAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,YAAY,CAAC,KAAK,CAAA;QAChD,IAAI,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;QACvC,IAAI,aAAa,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,EAAE;YAAE,MAAM,IAAI,aAAa,CAAC,wBAAwB,CAAC,CAAA;QACjG,iGAAiG;QACjG,IAAI,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;YACrE,QAAQ,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;QAC7F,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,cAAc,EAAE,CAAC,CAAA;QAC5L,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,CAAC;gBAAE,MAAM,KAAK,CAAA;YACrD,MAAM,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAA;YAC9B,IAAI,KAAK,IAAI,IAAI;gBAAE,MAAM,KAAK,CAAA;YAC9B,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAA;IACD,MAAM,OAAO,GAAiB;QAC5B,WAAW;QACX,OAAO,EAAE,KAAK,EAAC,KAAK,EAAC,EAAE;YACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;YAChC,MAAM,UAAU,GAAG,GAAG,QAAQ,IAAI,KAAK,CAAC,UAAU,EAAE,CAAA;YACpD,MAAM,KAAK,GAAG,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC3E,IAAI,KAAK,IAAI,IAAI;gBAAE,OAAO,KAAK,CAAA;YAC/B,IAAI,CAAC;gBAAC,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;YAAC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAC1F,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,CAAC;oBAAE,MAAM,KAAK,CAAA;gBACrD,MAAM,KAAK,GAAG,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;gBAC3E,IAAI,KAAK,IAAI,IAAI;oBAAE,MAAM,KAAK,CAAA;gBAC9B,OAAO,KAAK,CAAA;YACd,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,KAAK,EAAC,KAAK,EAAC,EAAE;YACtB,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,EAAE,SAAS,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,CAAA;YACnG,MAAM,QAAQ,GAAG,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,CAAC,CAAA;YAC7I,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,EAAE,GAAG,IAAI,CAAC,eAAe,EAAE,IAAI,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,IAAI,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAA;YACpK,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;QACxJ,CAAC;QACD,MAAM,EAAE,sBAAsB,CAAC,IAAI,CAAC;KACrC,CAAA;IACD,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,aAAa,CAAA;AACzD,eAAO,MAAM,wBAAwB,mBAAmB,CAAA;AACxD,eAAO,MAAM,4BAA4B,wBAAwB,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,+BAA+B,GAAG,UAAU,CAAA;AACzD,MAAM,CAAC,MAAM,wBAAwB,GAAG,gBAAgB,CAAA;AACxD,MAAM,CAAC,MAAM,4BAA4B,GAAG,qBAAqB,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAE5B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAE5B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type PlanningPlugin } from '@owlmeans/planning';
|
|
2
|
+
import type { PlanningAuthPluginOptions } from './types.js';
|
|
3
|
+
/** Register auth identity schemas; creation attribution is an explicit server-only integration. */
|
|
4
|
+
export declare const makePlanningAuthPlugin: (opts?: PlanningAuthPluginOptions) => PlanningPlugin;
|
|
5
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,KAAK,cAAc,EAAsB,MAAM,oBAAoB,CAAA;AAG5G,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAA;AAE3D,mGAAmG;AACnG,eAAO,MAAM,sBAAsB,UAAU,yBAAyB,KAAQ,cAuB5E,CAAA"}
|
package/build/plugin.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AssigneeKind, TransitionAction } from '@owlmeans/planning';
|
|
2
|
+
import { makePlanningAuth } from './adapter.js';
|
|
3
|
+
import { PLANNING_AUTH_HUMAN_TYPE, PLANNING_AUTH_NON_HUMAN_TYPE } from './consts.js';
|
|
4
|
+
/** Register auth identity schemas; creation attribution is an explicit server-only integration. */
|
|
5
|
+
export const makePlanningAuthPlugin = (opts = {}) => ({
|
|
6
|
+
name: opts.name ?? 'planning-auth',
|
|
7
|
+
order: opts.order ?? -100,
|
|
8
|
+
owns: opts.owns,
|
|
9
|
+
schemas: { assigneeTypes: [
|
|
10
|
+
{ type: opts.humanType ?? PLANNING_AUTH_HUMAN_TYPE, version: 1, kind: AssigneeKind.Human, fields: { type: 'object', additionalProperties: true }, authentication: 'required' },
|
|
11
|
+
{ type: opts.nonHumanType ?? PLANNING_AUTH_NON_HUMAN_TYPE, version: 1, kind: AssigneeKind.NonHuman, fields: { type: 'object', additionalProperties: true }, authentication: 'optional' },
|
|
12
|
+
] },
|
|
13
|
+
...(opts.attributeCreates === true ? {
|
|
14
|
+
before: async (exec, ctx) => {
|
|
15
|
+
if (exec.action !== TransitionAction.Create || typeof exec.card === 'string')
|
|
16
|
+
return;
|
|
17
|
+
const draft = exec.card;
|
|
18
|
+
const adapter = makePlanningAuth({ ...opts, planning: () => ctx.facade });
|
|
19
|
+
// createdBy was already stamped from the trusted actor by the executor. Trusted producers
|
|
20
|
+
// may name a known earlier creator; HTTP admission discards the caller's creator claim.
|
|
21
|
+
const reporter = draft.reporter != null || draft.createdBy == null ? undefined
|
|
22
|
+
: await adapter.assigneeFor({ externalId: draft.createdBy, nickname: `user-${draft.createdBy}` });
|
|
23
|
+
const responsible = draft.assignee != null || opts.defaultAssignee == null ? undefined
|
|
24
|
+
: await adapter.assigneeFor({ ...opts.defaultAssignee, kind: opts.defaultAssignee.kind ?? AssigneeKind.NonHuman });
|
|
25
|
+
if (reporter == null && responsible == null)
|
|
26
|
+
return;
|
|
27
|
+
return { ...exec, card: { ...draft, ...(reporter != null ? { reporter: reporter.id } : {}), ...(responsible != null ? { assignee: responsible.id } : {}) } };
|
|
28
|
+
},
|
|
29
|
+
} : {}),
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Assignee, AssigneeKind, PlanningFacade, PlanningPlugin, PlanningScope, Team } from '@owlmeans/planning';
|
|
2
|
+
export interface PlanningAuthIdentity {
|
|
3
|
+
externalId: string;
|
|
4
|
+
nickname: string;
|
|
5
|
+
kind?: AssigneeKind;
|
|
6
|
+
type?: string;
|
|
7
|
+
fields?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface PlanningAuthOptions {
|
|
10
|
+
/** Trusted, server-scoped facade; never construct from caller-supplied organization ids. */
|
|
11
|
+
planning: () => PlanningFacade;
|
|
12
|
+
provider?: string;
|
|
13
|
+
humanType?: string;
|
|
14
|
+
nonHumanType?: string;
|
|
15
|
+
defaultAssignee?: PlanningAuthIdentity;
|
|
16
|
+
}
|
|
17
|
+
export interface PlanningAuth {
|
|
18
|
+
assigneeFor: (identity: PlanningAuthIdentity) => Promise<Assignee>;
|
|
19
|
+
/** Resolves a planning team for a verified external group, without changing group membership. */
|
|
20
|
+
teamFor: (group: {
|
|
21
|
+
externalId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}) => Promise<Team>;
|
|
24
|
+
scopeFor: (scope: PlanningScope) => Promise<PlanningScope>;
|
|
25
|
+
plugin: PlanningPlugin;
|
|
26
|
+
}
|
|
27
|
+
export interface PlanningAuthPluginOptions extends Omit<PlanningAuthOptions, 'planning'> {
|
|
28
|
+
name?: string;
|
|
29
|
+
order?: number;
|
|
30
|
+
owns?: PlanningPlugin['owns'];
|
|
31
|
+
attributeCreates?: boolean;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAErH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,cAAc,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,oBAAoB,CAAA;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAClE,iGAAiG;IACjG,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvE,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,aAAa,CAAC,CAAA;IAC1D,MAAM,EAAE,cAAc,CAAA;CACvB;AAED,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC;IACtF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAA;IAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/planning-auth",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-rc.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Optional OwlMeans authentication identity and group links for the generic planning system.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -b",
|
|
9
|
+
"test": "bun test ./tests"
|
|
10
|
+
},
|
|
11
|
+
"main": "build/index.js",
|
|
12
|
+
"module": "build/index.js",
|
|
13
|
+
"types": "build/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./build/index.d.ts",
|
|
17
|
+
"import": "./build/index.js",
|
|
18
|
+
"default": "./build/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@owlmeans/planning": "^0.1.18-rc.22"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
26
|
+
"@types/bun": "^1.4.2",
|
|
27
|
+
"typescript": "^7.0.2",
|
|
28
|
+
"@owlmeans/server-planning": "^0.1.18-rc.25",
|
|
29
|
+
"@owlmeans/context": "^0.1.18-rc.34"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { makePlanningAuthPlugin } from './plugin.js'
|
|
2
|
+
import { AssigneeKind, FieldsInvalid, mentionHelper, WorkcardConflict } from '@owlmeans/planning'
|
|
3
|
+
import type { Assignee } from '@owlmeans/planning'
|
|
4
|
+
import { OWLMEANS_PLANNING_AUTH_PROVIDER, PLANNING_AUTH_HUMAN_TYPE, PLANNING_AUTH_NON_HUMAN_TYPE } from './consts.js'
|
|
5
|
+
import type { PlanningAuth, PlanningAuthIdentity, PlanningAuthOptions } from './types.js'
|
|
6
|
+
|
|
7
|
+
/** Authentication integration is an optional planning adapter; planning itself knows no user system. */
|
|
8
|
+
export const makePlanningAuth = (opts: PlanningAuthOptions): PlanningAuth => {
|
|
9
|
+
const provider = opts.provider ?? OWLMEANS_PLANNING_AUTH_PROVIDER
|
|
10
|
+
const humanType = opts.humanType ?? PLANNING_AUTH_HUMAN_TYPE
|
|
11
|
+
const nonHumanType = opts.nonHumanType ?? PLANNING_AUTH_NON_HUMAN_TYPE
|
|
12
|
+
const assigneeFor = async (identity: PlanningAuthIdentity): Promise<Assignee> => {
|
|
13
|
+
if (identity.externalId.trim() === '') throw new FieldsInvalid('auth:external-id-required')
|
|
14
|
+
const authentication = { provider, externalId: identity.externalId }
|
|
15
|
+
const planning = opts.planning()
|
|
16
|
+
const existing = async () => (await planning.assignees.list({ authentication, size: 1 })).items[0]
|
|
17
|
+
const found = await existing()
|
|
18
|
+
if (found != null) return found
|
|
19
|
+
const kind = identity.kind ?? AssigneeKind.Human
|
|
20
|
+
let nickname = identity.nickname.trim()
|
|
21
|
+
if (mentionHelper.nicknameKey(nickname) === '') throw new FieldsInvalid('auth:nickname-required')
|
|
22
|
+
// A nickname collision must never link an existing person to a different authentication subject.
|
|
23
|
+
if ((await planning.assignees.list({ nickname, size: 1 })).total > 0) {
|
|
24
|
+
nickname = `${nickname.slice(0, 64)}-${encodeURIComponent(identity.externalId).slice(-48)}`
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await planning.assignees.create({ nickname, kind, type: identity.type ?? (kind === AssigneeKind.Human ? humanType : nonHumanType), fields: identity.fields ?? {}, authentication })
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!(error instanceof WorkcardConflict)) throw error
|
|
30
|
+
const raced = await existing()
|
|
31
|
+
if (raced == null) throw error
|
|
32
|
+
return raced
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const adapter: PlanningAuth = {
|
|
36
|
+
assigneeFor,
|
|
37
|
+
teamFor: async group => {
|
|
38
|
+
const planning = opts.planning()
|
|
39
|
+
const externalId = `${provider}:${group.externalId}`
|
|
40
|
+
const found = (await planning.teams.list({ externalId, size: 1 })).items[0]
|
|
41
|
+
if (found != null) return found
|
|
42
|
+
try { return await planning.teams.create({ name: group.name, externalId }) } catch (error) {
|
|
43
|
+
if (!(error instanceof WorkcardConflict)) throw error
|
|
44
|
+
const raced = (await planning.teams.list({ externalId, size: 1 })).items[0]
|
|
45
|
+
if (raced == null) throw error
|
|
46
|
+
return raced
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
scopeFor: async scope => {
|
|
50
|
+
const externalId = scope.profileId ?? scope.userId ?? scope.actor?.profileId ?? scope.actor?.userId
|
|
51
|
+
const reporter = externalId == null ? undefined : await assigneeFor({ externalId, nickname: `user-${externalId}`, kind: AssigneeKind.Human })
|
|
52
|
+
const assignee = opts.defaultAssignee == null ? undefined : await assigneeFor({ ...opts.defaultAssignee, kind: opts.defaultAssignee.kind ?? AssigneeKind.NonHuman })
|
|
53
|
+
return { ...scope, ...(reporter?.id != null ? { assigneeId: reporter.id } : {}), ...(assignee?.id != null ? { defaultAssigneeId: assignee.id } : {}) }
|
|
54
|
+
},
|
|
55
|
+
plugin: makePlanningAuthPlugin(opts),
|
|
56
|
+
}
|
|
57
|
+
return adapter
|
|
58
|
+
}
|
package/src/consts.ts
ADDED
package/src/index.ts
ADDED
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AssigneeKind, TransitionAction, type PlanningPlugin, type WorkcardDraft } from '@owlmeans/planning'
|
|
2
|
+
import { makePlanningAuth } from './adapter.js'
|
|
3
|
+
import { PLANNING_AUTH_HUMAN_TYPE, PLANNING_AUTH_NON_HUMAN_TYPE } from './consts.js'
|
|
4
|
+
import type { PlanningAuthPluginOptions } from './types.js'
|
|
5
|
+
|
|
6
|
+
/** Register auth identity schemas; creation attribution is an explicit server-only integration. */
|
|
7
|
+
export const makePlanningAuthPlugin = (opts: PlanningAuthPluginOptions = {}): PlanningPlugin => ({
|
|
8
|
+
name: opts.name ?? 'planning-auth',
|
|
9
|
+
order: opts.order ?? -100,
|
|
10
|
+
owns: opts.owns,
|
|
11
|
+
schemas: { assigneeTypes: [
|
|
12
|
+
{ type: opts.humanType ?? PLANNING_AUTH_HUMAN_TYPE, version: 1, kind: AssigneeKind.Human, fields: { type: 'object', additionalProperties: true }, authentication: 'required' },
|
|
13
|
+
{ type: opts.nonHumanType ?? PLANNING_AUTH_NON_HUMAN_TYPE, version: 1, kind: AssigneeKind.NonHuman, fields: { type: 'object', additionalProperties: true }, authentication: 'optional' },
|
|
14
|
+
] },
|
|
15
|
+
...(opts.attributeCreates === true ? {
|
|
16
|
+
before: async (exec, ctx) => {
|
|
17
|
+
if (exec.action !== TransitionAction.Create || typeof exec.card === 'string') return
|
|
18
|
+
const draft = exec.card as WorkcardDraft
|
|
19
|
+
const adapter = makePlanningAuth({ ...opts, planning: () => ctx.facade })
|
|
20
|
+
// createdBy was already stamped from the trusted actor by the executor. Trusted producers
|
|
21
|
+
// may name a known earlier creator; HTTP admission discards the caller's creator claim.
|
|
22
|
+
const reporter = draft.reporter != null || draft.createdBy == null ? undefined
|
|
23
|
+
: await adapter.assigneeFor({ externalId: draft.createdBy, nickname: `user-${draft.createdBy}` })
|
|
24
|
+
const responsible = draft.assignee != null || opts.defaultAssignee == null ? undefined
|
|
25
|
+
: await adapter.assigneeFor({ ...opts.defaultAssignee, kind: opts.defaultAssignee.kind ?? AssigneeKind.NonHuman })
|
|
26
|
+
if (reporter == null && responsible == null) return
|
|
27
|
+
return { ...exec, card: { ...draft, ...(reporter != null ? { reporter: reporter.id } : {}), ...(responsible != null ? { assignee: responsible.id } : {}) } }
|
|
28
|
+
},
|
|
29
|
+
} : {}),
|
|
30
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Assignee, AssigneeKind, PlanningFacade, PlanningPlugin, PlanningScope, Team } from '@owlmeans/planning'
|
|
2
|
+
|
|
3
|
+
export interface PlanningAuthIdentity {
|
|
4
|
+
externalId: string
|
|
5
|
+
nickname: string
|
|
6
|
+
kind?: AssigneeKind
|
|
7
|
+
type?: string
|
|
8
|
+
fields?: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PlanningAuthOptions {
|
|
12
|
+
/** Trusted, server-scoped facade; never construct from caller-supplied organization ids. */
|
|
13
|
+
planning: () => PlanningFacade
|
|
14
|
+
provider?: string
|
|
15
|
+
humanType?: string
|
|
16
|
+
nonHumanType?: string
|
|
17
|
+
defaultAssignee?: PlanningAuthIdentity
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PlanningAuth {
|
|
21
|
+
assigneeFor: (identity: PlanningAuthIdentity) => Promise<Assignee>
|
|
22
|
+
/** Resolves a planning team for a verified external group, without changing group membership. */
|
|
23
|
+
teamFor: (group: { externalId: string, name: string }) => Promise<Team>
|
|
24
|
+
scopeFor: (scope: PlanningScope) => Promise<PlanningScope>
|
|
25
|
+
plugin: PlanningPlugin
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanningAuthPluginOptions extends Omit<PlanningAuthOptions, 'planning'> {
|
|
29
|
+
name?: string
|
|
30
|
+
order?: number
|
|
31
|
+
owns?: PlanningPlugin['owns']
|
|
32
|
+
attributeCreates?: boolean
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AppType, makeBasicContext, type BasicConfig } from '@owlmeans/context'
|
|
3
|
+
import { AssigneeKind, type PlanningService } from '@owlmeans/planning'
|
|
4
|
+
import { appendPlanningService, makeMemoryPlanningStore } from '@owlmeans/server-planning'
|
|
5
|
+
import { makePlanningAuth, makePlanningAuthPlugin } from '../src/index.js'
|
|
6
|
+
|
|
7
|
+
const boot = async () => {
|
|
8
|
+
const ctx = appendPlanningService(makeBasicContext<BasicConfig>({ ready: false, service: 'auth-tests', type: AppType.Backend, services: {} }), {
|
|
9
|
+
store: makeMemoryPlanningStore(), plugins: [makePlanningAuthPlugin()],
|
|
10
|
+
})
|
|
11
|
+
await ctx.configure().init()
|
|
12
|
+
return ctx.planning() as PlanningService
|
|
13
|
+
}
|
|
14
|
+
describe('@owlmeans/planning-auth — optional identity integration', () => {
|
|
15
|
+
test('provider plus subject is stable across nickname and group-name changes', async () => {
|
|
16
|
+
const planning = await boot(), p = planning.for({ entityId: 'organization-1' })
|
|
17
|
+
const auth = makePlanningAuth({ planning: () => p, provider: 'external-oidc' })
|
|
18
|
+
const actor = await auth.assigneeFor({ externalId: 'subject-1', nickname: 'Alice' })
|
|
19
|
+
const again = await auth.assigneeFor({ externalId: 'subject-1', nickname: 'Changed display name' })
|
|
20
|
+
expect(again.id).toBe(actor.id)
|
|
21
|
+
expect(again.kind).toBe(AssigneeKind.Human)
|
|
22
|
+
const collision = await auth.assigneeFor({ externalId: 'subject-2', nickname: 'ALICE' })
|
|
23
|
+
expect(collision.id).not.toBe(actor.id)
|
|
24
|
+
expect(collision.nickname.toLowerCase()).not.toBe('alice')
|
|
25
|
+
const team = await auth.teamFor({ externalId: 'group-1', name: 'Editors' })
|
|
26
|
+
expect((await auth.teamFor({ externalId: 'group-1', name: 'Renamed editors' })).id).toBe(team.id)
|
|
27
|
+
expect(team.externalId).toBe('external-oidc:group-1')
|
|
28
|
+
expect(await p.teams.members(team.id!)).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
test('trusted scope resolves a human reporter and a non-human default assignee per organization', async () => {
|
|
31
|
+
const planning = await boot(), p = planning.for({ entityId: 'organization-1' })
|
|
32
|
+
const adapter = makePlanningAuth({ planning: () => p, defaultAssignee: { externalId: 'agent-runner', nickname: 'agent-runner' } })
|
|
33
|
+
const scope = await adapter.scopeFor({ entityId: 'organization-1', profileId: 'app:user-1', channel: 'web' })
|
|
34
|
+
const reporter = await p.assignees.get(scope.assigneeId!), assignee = await p.assignees.get(scope.defaultAssigneeId!)
|
|
35
|
+
expect(reporter.authentication).toEqual({ provider: 'owlmeans', externalId: 'app:user-1' })
|
|
36
|
+
expect(assignee.kind).toBe(AssigneeKind.NonHuman)
|
|
37
|
+
const concurrent = await Promise.all(Array.from({ length: 5 }, () => adapter.scopeFor(scope)))
|
|
38
|
+
expect(new Set(concurrent.map(row => row.assigneeId)).size).toBe(1)
|
|
39
|
+
const other = makePlanningAuth({ planning: () => planning.for({ entityId: 'organization-2' }), defaultAssignee: { externalId: 'agent-runner', nickname: 'agent-runner' } })
|
|
40
|
+
const separate = await other.scopeFor({ entityId: 'organization-2', profileId: 'app:user-1' })
|
|
41
|
+
expect(separate.assigneeId).not.toBe(scope.assigneeId)
|
|
42
|
+
expect(separate.defaultAssigneeId).not.toBe(scope.defaultAssigneeId)
|
|
43
|
+
})
|
|
44
|
+
})
|