@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 CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # @owlmeans/planning-auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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"}
@@ -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":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const OWLMEANS_PLANNING_AUTH_PROVIDER = "owlmeans";
2
+ export declare const PLANNING_AUTH_HUMAN_TYPE = "planning:human";
3
+ export declare const PLANNING_AUTH_NON_HUMAN_TYPE = "planning:automation";
4
+ //# sourceMappingURL=consts.d.ts.map
@@ -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"}
@@ -0,0 +1,4 @@
1
+ export const OWLMEANS_PLANNING_AUTH_PROVIDER = 'owlmeans';
2
+ export const PLANNING_AUTH_HUMAN_TYPE = 'planning:human';
3
+ export const PLANNING_AUTH_NON_HUMAN_TYPE = 'planning:automation';
4
+ //# sourceMappingURL=consts.js.map
@@ -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"}
@@ -0,0 +1,5 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './adapter.js';
4
+ export * from './plugin.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,4 @@
1
+ export * from './consts.js';
2
+ export * from './adapter.js';
3
+ export * from './plugin.js';
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
@@ -0,0 +1,3 @@
1
+ export const OWLMEANS_PLANNING_AUTH_PROVIDER = 'owlmeans'
2
+ export const PLANNING_AUTH_HUMAN_TYPE = 'planning:human'
3
+ export const PLANNING_AUTH_NON_HUMAN_TYPE = 'planning:automation'
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type * from './types.js'
2
+ export * from './consts.js'
3
+ export * from './adapter.js'
4
+
5
+ export * from './plugin.js'
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
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": ["@owlmeans/dep-config/tsconfig.base.json"],
3
+ "compilerOptions": { "rootDir": "./src/", "outDir": "./build/" },
4
+ "exclude": ["./build/**/*", "./tests/**/*"]
5
+ }