@plandesk/api 0.20.0 → 1.0.0-beta.2

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 (101) hide show
  1. package/dist/access-control.d.ts +150 -0
  2. package/dist/access-control.js +63 -0
  3. package/dist/agent-keys.d.ts +81 -0
  4. package/dist/agent-keys.js +181 -0
  5. package/dist/auth-context.d.ts +60 -0
  6. package/dist/auth-context.js +41 -0
  7. package/dist/auth.d.ts +33 -0
  8. package/dist/auth.js +357 -0
  9. package/dist/better-auth.d.ts +66 -0
  10. package/dist/better-auth.js +106 -0
  11. package/dist/github.d.ts +57 -0
  12. package/dist/github.js +115 -0
  13. package/dist/hosted-auth.d.ts +23 -0
  14. package/dist/hosted-auth.js +49 -0
  15. package/dist/identity.d.ts +38 -0
  16. package/dist/identity.js +163 -0
  17. package/dist/index.d.ts +13 -2
  18. package/dist/index.js +15 -3
  19. package/dist/invitations.d.ts +94 -0
  20. package/dist/invitations.js +211 -0
  21. package/dist/organizations.d.ts +32 -0
  22. package/dist/organizations.js +63 -0
  23. package/dist/permissions.d.ts +19 -0
  24. package/dist/permissions.js +42 -0
  25. package/dist/projection.d.ts +1 -1
  26. package/dist/projection.js +5 -5
  27. package/dist/routes/agent-runs.js +3 -3
  28. package/dist/routes/artifacts.js +6 -6
  29. package/dist/routes/auth.d.ts +13 -0
  30. package/dist/routes/auth.js +83 -0
  31. package/dist/routes/canvas.js +5 -5
  32. package/dist/routes/comments.js +11 -11
  33. package/dist/routes/documents.js +10 -10
  34. package/dist/routes/folders.js +8 -8
  35. package/dist/routes/goals.js +11 -11
  36. package/dist/routes/notes.js +8 -8
  37. package/dist/routes/orgs.d.ts +17 -0
  38. package/dist/routes/orgs.js +241 -0
  39. package/dist/routes/projects.js +13 -13
  40. package/dist/routes/shares.js +92 -3
  41. package/dist/routes/submissions.js +7 -8
  42. package/dist/routes/tags.js +6 -6
  43. package/dist/routes/tasks.js +14 -3
  44. package/dist/routes/tokens.js +4 -4
  45. package/dist/serialize.d.ts +3 -3
  46. package/dist/server.d.ts +20 -2
  47. package/dist/server.js +57 -9
  48. package/dist/services/agent-runs.d.ts +13 -14
  49. package/dist/services/agent-runs.js +39 -41
  50. package/dist/services/artifacts.d.ts +6 -7
  51. package/dist/services/artifacts.js +30 -26
  52. package/dist/services/canvas.d.ts +11 -12
  53. package/dist/services/canvas.js +58 -41
  54. package/dist/services/comments.d.ts +11 -12
  55. package/dist/services/comments.js +44 -64
  56. package/dist/services/documents.d.ts +10 -11
  57. package/dist/services/documents.js +74 -55
  58. package/dist/services/files.d.ts +2 -1
  59. package/dist/services/files.js +13 -5
  60. package/dist/services/folders.d.ts +7 -8
  61. package/dist/services/folders.js +45 -33
  62. package/dist/services/goals.d.ts +24 -25
  63. package/dist/services/goals.js +60 -53
  64. package/dist/services/index.d.ts +2 -5
  65. package/dist/services/index.js +15 -20
  66. package/dist/services/notes.d.ts +7 -8
  67. package/dist/services/notes.js +35 -23
  68. package/dist/services/org-scope.d.ts +22 -0
  69. package/dist/services/org-scope.js +38 -0
  70. package/dist/services/projects.d.ts +12 -13
  71. package/dist/services/projects.js +90 -58
  72. package/dist/services/scope.d.ts +12 -0
  73. package/dist/services/scope.js +29 -0
  74. package/dist/services/share.d.ts +74 -10
  75. package/dist/services/share.js +213 -23
  76. package/dist/services/sync.d.ts +7 -29
  77. package/dist/services/sync.js +37 -142
  78. package/dist/services/tags.d.ts +6 -7
  79. package/dist/services/tags.js +34 -22
  80. package/dist/services/tasks.d.ts +24 -17
  81. package/dist/services/tasks.js +97 -58
  82. package/dist/services/tokens.d.ts +6 -5
  83. package/dist/services/tokens.js +13 -6
  84. package/dist/session.d.ts +5 -0
  85. package/dist/session.js +8 -0
  86. package/dist/storage/local.js +16 -10
  87. package/dist/storage/s3.js +11 -6
  88. package/dist/test-ba-org.d.ts +16 -0
  89. package/dist/test-ba-org.js +35 -0
  90. package/dist/test-helpers.d.ts +21 -5
  91. package/dist/test-helpers.js +45 -8
  92. package/dist/vercel.d.ts +3 -0
  93. package/dist/vercel.js +62 -0
  94. package/dist/worker.d.ts +44 -0
  95. package/dist/worker.js +106 -0
  96. package/package.json +8 -5
  97. package/web/assets/index-D29SyYFS.css +2 -0
  98. package/web/assets/index-tBJ_K3ur.js +358 -0
  99. package/web/index.html +2 -2
  100. package/web/assets/index-CeBvZ-aq.css +0 -2
  101. package/web/assets/index-CuRAxImC.js +0 -358
@@ -0,0 +1,150 @@
1
+ export declare const statement: {
2
+ readonly task: readonly ["read", "create", "update", "delete"];
3
+ readonly document: readonly ["read", "create", "update", "delete"];
4
+ readonly edge: readonly ["read", "create", "update", "delete"];
5
+ readonly goal: readonly ["read", "create", "update", "delete"];
6
+ readonly comment: readonly ["read", "create", "update", "delete"];
7
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
8
+ readonly project: readonly ["create", "delete"];
9
+ readonly organization: readonly ["update", "delete"];
10
+ readonly member: readonly ["create", "update", "delete"];
11
+ readonly invitation: readonly ["create", "cancel"];
12
+ readonly team: readonly ["create", "update", "delete"];
13
+ readonly ac: readonly ["create", "read", "update", "delete"];
14
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
15
+ };
16
+ export declare const ac: {
17
+ newRole<const TRoleStatements extends import("better-auth/plugins/access").Statements>(statements: import("better-auth/plugins/access").RoleInput<{
18
+ readonly task: readonly ["read", "create", "update", "delete"];
19
+ readonly document: readonly ["read", "create", "update", "delete"];
20
+ readonly edge: readonly ["read", "create", "update", "delete"];
21
+ readonly goal: readonly ["read", "create", "update", "delete"];
22
+ readonly comment: readonly ["read", "create", "update", "delete"];
23
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
24
+ readonly project: readonly ["create", "delete"];
25
+ readonly organization: readonly ["update", "delete"];
26
+ readonly member: readonly ["create", "update", "delete"];
27
+ readonly invitation: readonly ["create", "cancel"];
28
+ readonly team: readonly ["create", "update", "delete"];
29
+ readonly ac: readonly ["create", "read", "update", "delete"];
30
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
31
+ }, TRoleStatements>): import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<TRoleStatements>, {
32
+ readonly task: readonly ["read", "create", "update", "delete"];
33
+ readonly document: readonly ["read", "create", "update", "delete"];
34
+ readonly edge: readonly ["read", "create", "update", "delete"];
35
+ readonly goal: readonly ["read", "create", "update", "delete"];
36
+ readonly comment: readonly ["read", "create", "update", "delete"];
37
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
38
+ readonly project: readonly ["create", "delete"];
39
+ readonly organization: readonly ["update", "delete"];
40
+ readonly member: readonly ["create", "update", "delete"];
41
+ readonly invitation: readonly ["create", "cancel"];
42
+ readonly team: readonly ["create", "update", "delete"];
43
+ readonly ac: readonly ["create", "read", "update", "delete"];
44
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
45
+ }>;
46
+ statements: {
47
+ readonly task: readonly ["read", "create", "update", "delete"];
48
+ readonly document: readonly ["read", "create", "update", "delete"];
49
+ readonly edge: readonly ["read", "create", "update", "delete"];
50
+ readonly goal: readonly ["read", "create", "update", "delete"];
51
+ readonly comment: readonly ["read", "create", "update", "delete"];
52
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
53
+ readonly project: readonly ["create", "delete"];
54
+ readonly organization: readonly ["update", "delete"];
55
+ readonly member: readonly ["create", "update", "delete"];
56
+ readonly invitation: readonly ["create", "cancel"];
57
+ readonly team: readonly ["create", "update", "delete"];
58
+ readonly ac: readonly ["create", "read", "update", "delete"];
59
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
60
+ };
61
+ };
62
+ export declare const member: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
63
+ readonly task: readonly ["read", "create", "update", "delete"];
64
+ readonly document: readonly ["read", "create", "update", "delete"];
65
+ readonly edge: readonly ["read", "create", "update", "delete"];
66
+ readonly goal: readonly ["read", "create", "update", "delete"];
67
+ readonly comment: readonly ["read", "create", "update", "delete"];
68
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
69
+ readonly project: readonly [];
70
+ readonly organization: readonly [];
71
+ readonly member: readonly [];
72
+ readonly invitation: readonly [];
73
+ readonly team: readonly [];
74
+ readonly ac: readonly [];
75
+ readonly apiKey: readonly [];
76
+ }>, {
77
+ readonly task: readonly ["read", "create", "update", "delete"];
78
+ readonly document: readonly ["read", "create", "update", "delete"];
79
+ readonly edge: readonly ["read", "create", "update", "delete"];
80
+ readonly goal: readonly ["read", "create", "update", "delete"];
81
+ readonly comment: readonly ["read", "create", "update", "delete"];
82
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
83
+ readonly project: readonly ["create", "delete"];
84
+ readonly organization: readonly ["update", "delete"];
85
+ readonly member: readonly ["create", "update", "delete"];
86
+ readonly invitation: readonly ["create", "cancel"];
87
+ readonly team: readonly ["create", "update", "delete"];
88
+ readonly ac: readonly ["create", "read", "update", "delete"];
89
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
90
+ }>;
91
+ export declare const admin: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
92
+ readonly project: ["create", "delete"];
93
+ readonly task: readonly ["read", "create", "update", "delete"];
94
+ readonly document: readonly ["read", "create", "update", "delete"];
95
+ readonly edge: readonly ["read", "create", "update", "delete"];
96
+ readonly goal: readonly ["read", "create", "update", "delete"];
97
+ readonly comment: readonly ["read", "create", "update", "delete"];
98
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
99
+ readonly organization: readonly [];
100
+ readonly member: readonly [];
101
+ readonly invitation: readonly [];
102
+ readonly team: readonly [];
103
+ readonly ac: readonly [];
104
+ readonly apiKey: readonly [];
105
+ }>, {
106
+ readonly task: readonly ["read", "create", "update", "delete"];
107
+ readonly document: readonly ["read", "create", "update", "delete"];
108
+ readonly edge: readonly ["read", "create", "update", "delete"];
109
+ readonly goal: readonly ["read", "create", "update", "delete"];
110
+ readonly comment: readonly ["read", "create", "update", "delete"];
111
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
112
+ readonly project: readonly ["create", "delete"];
113
+ readonly organization: readonly ["update", "delete"];
114
+ readonly member: readonly ["create", "update", "delete"];
115
+ readonly invitation: readonly ["create", "cancel"];
116
+ readonly team: readonly ["create", "update", "delete"];
117
+ readonly ac: readonly ["create", "read", "update", "delete"];
118
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
119
+ }>;
120
+ export declare const owner: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
121
+ readonly project: ["create", "delete"];
122
+ readonly organization: ["update", "delete"];
123
+ readonly member: ["create", "update", "delete"];
124
+ readonly invitation: ["create", "cancel"];
125
+ readonly team: ["create", "update", "delete"];
126
+ readonly ac: ["create", "read", "update", "delete"];
127
+ readonly apiKey: ["create", "read", "update", "delete"];
128
+ readonly task: readonly ["read", "create", "update", "delete"];
129
+ readonly document: readonly ["read", "create", "update", "delete"];
130
+ readonly edge: readonly ["read", "create", "update", "delete"];
131
+ readonly goal: readonly ["read", "create", "update", "delete"];
132
+ readonly comment: readonly ["read", "create", "update", "delete"];
133
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
134
+ }>, {
135
+ readonly task: readonly ["read", "create", "update", "delete"];
136
+ readonly document: readonly ["read", "create", "update", "delete"];
137
+ readonly edge: readonly ["read", "create", "update", "delete"];
138
+ readonly goal: readonly ["read", "create", "update", "delete"];
139
+ readonly comment: readonly ["read", "create", "update", "delete"];
140
+ readonly agent_run: readonly ["read", "create", "update", "delete"];
141
+ readonly project: readonly ["create", "delete"];
142
+ readonly organization: readonly ["update", "delete"];
143
+ readonly member: readonly ["create", "update", "delete"];
144
+ readonly invitation: readonly ["create", "cancel"];
145
+ readonly team: readonly ["create", "update", "delete"];
146
+ readonly ac: readonly ["create", "read", "update", "delete"];
147
+ readonly apiKey: readonly ["create", "read", "update", "delete"];
148
+ }>;
149
+ export declare function intersectPermissions(a: Record<string, readonly string[]>, b: Record<string, readonly string[]>): Record<string, string[]>;
150
+ //# sourceMappingURL=access-control.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { createAccessControl } from 'better-auth/plugins/access';
2
+ const workActions = ['read', 'create', 'update', 'delete'];
3
+ export const statement = {
4
+ task: workActions,
5
+ document: workActions,
6
+ edge: workActions,
7
+ goal: workActions,
8
+ comment: workActions,
9
+ agent_run: workActions,
10
+ project: ['create', 'delete'],
11
+ organization: ['update', 'delete'],
12
+ member: ['create', 'update', 'delete'],
13
+ invitation: ['create', 'cancel'],
14
+ team: ['create', 'update', 'delete'],
15
+ ac: ['create', 'read', 'update', 'delete'],
16
+ apiKey: ['create', 'read', 'update', 'delete'],
17
+ };
18
+ export const ac = createAccessControl(statement);
19
+ const memberPermissions = {
20
+ task: workActions,
21
+ document: workActions,
22
+ edge: workActions,
23
+ goal: workActions,
24
+ comment: workActions,
25
+ agent_run: workActions,
26
+ project: [],
27
+ organization: [],
28
+ member: [],
29
+ invitation: [],
30
+ team: [],
31
+ ac: [],
32
+ apiKey: [],
33
+ };
34
+ export const member = ac.newRole(memberPermissions);
35
+ export const admin = ac.newRole({
36
+ ...memberPermissions,
37
+ project: ['create', 'delete'],
38
+ });
39
+ export const owner = ac.newRole({
40
+ ...memberPermissions,
41
+ project: ['create', 'delete'],
42
+ organization: ['update', 'delete'],
43
+ member: ['create', 'update', 'delete'],
44
+ invitation: ['create', 'cancel'],
45
+ team: ['create', 'update', 'delete'],
46
+ ac: ['create', 'read', 'update', 'delete'],
47
+ apiKey: ['create', 'read', 'update', 'delete'],
48
+ });
49
+ export function intersectPermissions(a, b) {
50
+ const intersection = {};
51
+ for (const [resource, actions] of Object.entries(a)) {
52
+ const otherActions = b[resource];
53
+ if (otherActions === undefined) {
54
+ continue;
55
+ }
56
+ const sharedActions = actions.filter((action) => otherActions.includes(action));
57
+ if (sharedActions.length > 0) {
58
+ intersection[resource] = sharedActions;
59
+ }
60
+ }
61
+ return intersection;
62
+ }
63
+ //# sourceMappingURL=access-control.js.map
@@ -0,0 +1,81 @@
1
+ import type { OrgRole } from '@plandesk/db';
2
+ import type { BetterAuthInstance } from './better-auth.js';
3
+ import { type PermissionSet } from './permissions.js';
4
+ /** Key profile stored in better-auth metadata. Absent/unknown → agent (BA4b-1). */
5
+ export type ApiKeyKind = 'agent' | 'owner';
6
+ /**
7
+ * Default agent grant: work resources full + project create.
8
+ * member / organization / apiKey are never granted at mint (escalation closed).
9
+ */
10
+ export declare const DEFAULT_AGENT_KEY_PERMISSIONS: PermissionSet;
11
+ /** Full owner permission set — default grant for org-wide owner keys (BA4b-1). */
12
+ export declare const DEFAULT_OWNER_KEY_PERMISSIONS: PermissionSet;
13
+ /** Always stripped from agent-key effective perms — a key must never mint keys. */
14
+ export declare const AGENT_FORBIDDEN_RESOURCES: readonly ["apiKey"];
15
+ /**
16
+ * Live-role ceiling at verify time (BA5 + BA4b-1):
17
+ * effective = intersect(keyPermissions, liveMemberRole);
18
+ * agent profile: then strip AGENT_FORBIDDEN_RESOURCES (apiKey).
19
+ * owner profile: retain apiKey if both key and live role still grant it.
20
+ * No live role (member removed) → empty set.
21
+ */
22
+ export declare function applyAgentKeyPermissionCeiling(keyPermissions: Record<string, readonly string[]> | null | undefined, liveRole: OrgRole | undefined, kind?: ApiKeyKind): PermissionSet;
23
+ export type VerifiedApiKey = {
24
+ valid: boolean;
25
+ referenceId: string;
26
+ permissions: Record<string, string[]> | null;
27
+ metadata: unknown;
28
+ };
29
+ /**
30
+ * Call better-auth verifyApiKey without relying on erased plugin types on Auth.
31
+ * Runtime-validates the result shape so TypeScript stays cast-free.
32
+ */
33
+ export declare function verifyBetterAuthApiKey(auth: BetterAuthInstance, key: string): Promise<VerifiedApiKey | undefined>;
34
+ export type CreateScopedAgentKeyInput = {
35
+ auth: BetterAuthInstance;
36
+ userId: string;
37
+ orgId: string;
38
+ projectId: string;
39
+ permissions?: PermissionSet;
40
+ name?: string;
41
+ };
42
+ export type CreatedScopedAgentKey = {
43
+ id: string;
44
+ key: string;
45
+ name: string | null;
46
+ permissions: Record<string, string[]> | null;
47
+ metadata: {
48
+ projectId: string;
49
+ orgId: string;
50
+ };
51
+ };
52
+ /**
53
+ * Server-side mint for a project-scoped agent key.
54
+ * Call only after the caller is a session-authenticated owner (route enforces).
55
+ * Metadata has no `kind` (or may omit it) — resolver treats absent as agent.
56
+ */
57
+ export declare function createScopedAgentKey(input: CreateScopedAgentKeyInput): Promise<CreatedScopedAgentKey>;
58
+ export type CreateOrgOwnerKeyInput = {
59
+ auth: BetterAuthInstance;
60
+ userId: string;
61
+ orgId: string;
62
+ permissions?: PermissionSet;
63
+ name?: string;
64
+ };
65
+ export type CreatedOrgOwnerKey = {
66
+ id: string;
67
+ key: string;
68
+ name: string | null;
69
+ permissions: Record<string, string[]> | null;
70
+ metadata: {
71
+ orgId: string;
72
+ kind: 'owner';
73
+ };
74
+ };
75
+ /**
76
+ * Server-side mint for an org-wide owner key (BA4b-1).
77
+ * No projectId in metadata → org-wide reach. kind: 'owner' retains apiKey
78
+ * after the live-role ceiling (still intersected with live role).
79
+ */
80
+ export declare function createOrgOwnerKey(input: CreateOrgOwnerKeyInput): Promise<CreatedOrgOwnerKey>;
81
+ //# sourceMappingURL=agent-keys.d.ts.map
@@ -0,0 +1,181 @@
1
+ import { intersectPermissions } from './access-control.js';
2
+ import { orgRoleToPermissionSet } from './permissions.js';
3
+ const workActions = ['read', 'create', 'update', 'delete'];
4
+ /**
5
+ * Default agent grant: work resources full + project create.
6
+ * member / organization / apiKey are never granted at mint (escalation closed).
7
+ */
8
+ export const DEFAULT_AGENT_KEY_PERMISSIONS = {
9
+ task: workActions,
10
+ document: workActions,
11
+ edge: workActions,
12
+ goal: workActions,
13
+ comment: workActions,
14
+ agent_run: workActions,
15
+ project: ['create'],
16
+ member: [],
17
+ organization: [],
18
+ apiKey: [],
19
+ };
20
+ /** Full owner permission set — default grant for org-wide owner keys (BA4b-1). */
21
+ export const DEFAULT_OWNER_KEY_PERMISSIONS = orgRoleToPermissionSet('owner');
22
+ /** Always stripped from agent-key effective perms — a key must never mint keys. */
23
+ export const AGENT_FORBIDDEN_RESOURCES = ['apiKey'];
24
+ /**
25
+ * Live-role ceiling at verify time (BA5 + BA4b-1):
26
+ * effective = intersect(keyPermissions, liveMemberRole);
27
+ * agent profile: then strip AGENT_FORBIDDEN_RESOURCES (apiKey).
28
+ * owner profile: retain apiKey if both key and live role still grant it.
29
+ * No live role (member removed) → empty set.
30
+ */
31
+ export function applyAgentKeyPermissionCeiling(keyPermissions, liveRole, kind = 'agent') {
32
+ const keyPerms = keyPermissions ?? {};
33
+ const rolePerms = liveRole === undefined ? {} : orgRoleToPermissionSet(liveRole);
34
+ const effective = intersectPermissions(keyPerms, rolePerms);
35
+ if (kind === 'agent') {
36
+ for (const resource of AGENT_FORBIDDEN_RESOURCES) {
37
+ delete effective[resource];
38
+ }
39
+ }
40
+ return effective;
41
+ }
42
+ function compactPermissions(permissions) {
43
+ const out = {};
44
+ for (const [resource, actions] of Object.entries(permissions)) {
45
+ if (actions.length > 0) {
46
+ out[resource] = [...actions];
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ function isRecord(value) {
52
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
53
+ }
54
+ function parsePermissions(value) {
55
+ if (value === null || value === undefined) {
56
+ return null;
57
+ }
58
+ if (!isRecord(value)) {
59
+ return null;
60
+ }
61
+ const out = {};
62
+ for (const [resource, actions] of Object.entries(value)) {
63
+ if (Array.isArray(actions) &&
64
+ actions.every((action) => typeof action === 'string')) {
65
+ out[resource] = actions;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Call better-auth verifyApiKey without relying on erased plugin types on Auth.
72
+ * Runtime-validates the result shape so TypeScript stays cast-free.
73
+ */
74
+ export async function verifyBetterAuthApiKey(auth, key) {
75
+ const api = auth.api;
76
+ if (!('verifyApiKey' in api)) {
77
+ return undefined;
78
+ }
79
+ const verify = Reflect.get(api, 'verifyApiKey');
80
+ if (typeof verify !== 'function') {
81
+ return undefined;
82
+ }
83
+ let raw;
84
+ try {
85
+ raw = await verify.call(api, { body: { key } });
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ if (!isRecord(raw) || typeof raw.valid !== 'boolean') {
91
+ return undefined;
92
+ }
93
+ if (raw.valid !== true) {
94
+ return undefined;
95
+ }
96
+ const keyBody = raw.key;
97
+ if (!isRecord(keyBody) || typeof keyBody.referenceId !== 'string') {
98
+ return undefined;
99
+ }
100
+ return {
101
+ valid: true,
102
+ referenceId: keyBody.referenceId,
103
+ permissions: parsePermissions(keyBody.permissions),
104
+ metadata: keyBody.metadata ?? null,
105
+ };
106
+ }
107
+ /**
108
+ * Shared better-auth createApiKey call (agent + owner mint paths).
109
+ */
110
+ async function mintBetterAuthApiKey(input) {
111
+ const api = input.auth.api;
112
+ if (!('createApiKey' in api)) {
113
+ throw new Error('better-auth api-key plugin is not mounted');
114
+ }
115
+ const create = Reflect.get(api, 'createApiKey');
116
+ if (typeof create !== 'function') {
117
+ throw new Error('better-auth createApiKey is not available');
118
+ }
119
+ const raw = await create.call(api, {
120
+ body: {
121
+ userId: input.userId,
122
+ name: input.name,
123
+ permissions: input.permissions,
124
+ metadata: input.metadata,
125
+ rateLimitEnabled: false,
126
+ },
127
+ });
128
+ if (!isRecord(raw) || typeof raw.id !== 'string' || typeof raw.key !== 'string') {
129
+ throw new Error('createApiKey did not return id/key');
130
+ }
131
+ if (raw.key.length === 0) {
132
+ throw new Error('createApiKey did not return a key');
133
+ }
134
+ return {
135
+ id: raw.id,
136
+ key: raw.key,
137
+ name: typeof raw.name === 'string' ? raw.name : null,
138
+ permissions: parsePermissions(raw.permissions),
139
+ };
140
+ }
141
+ /**
142
+ * Server-side mint for a project-scoped agent key.
143
+ * Call only after the caller is a session-authenticated owner (route enforces).
144
+ * Metadata has no `kind` (or may omit it) — resolver treats absent as agent.
145
+ */
146
+ export async function createScopedAgentKey(input) {
147
+ const permissions = compactPermissions(input.permissions ?? DEFAULT_AGENT_KEY_PERMISSIONS);
148
+ const metadata = { projectId: input.projectId, orgId: input.orgId };
149
+ const minted = await mintBetterAuthApiKey({
150
+ auth: input.auth,
151
+ userId: input.userId,
152
+ name: input.name ?? 'agent',
153
+ permissions,
154
+ metadata,
155
+ });
156
+ return {
157
+ ...minted,
158
+ metadata,
159
+ };
160
+ }
161
+ /**
162
+ * Server-side mint for an org-wide owner key (BA4b-1).
163
+ * No projectId in metadata → org-wide reach. kind: 'owner' retains apiKey
164
+ * after the live-role ceiling (still intersected with live role).
165
+ */
166
+ export async function createOrgOwnerKey(input) {
167
+ const permissions = compactPermissions(input.permissions ?? DEFAULT_OWNER_KEY_PERMISSIONS);
168
+ const metadata = { orgId: input.orgId, kind: 'owner' };
169
+ const minted = await mintBetterAuthApiKey({
170
+ auth: input.auth,
171
+ userId: input.userId,
172
+ name: input.name ?? 'owner',
173
+ permissions,
174
+ metadata,
175
+ });
176
+ return {
177
+ ...minted,
178
+ metadata,
179
+ };
180
+ }
181
+ //# sourceMappingURL=agent-keys.js.map
@@ -0,0 +1,60 @@
1
+ import type { OrgRole } from '@plandesk/db';
2
+ import { type PermissionSet } from './permissions.js';
3
+ /**
4
+ * Request-scoped auth. Middleware resolves org + effective permission once,
5
+ * whatever the transport; services read `orgId` and call requirePermission and
6
+ * must never branch on `kind`. Three transports, one identity model:
7
+ * browser session cookie, CLI/agent token (better-auth apiKey), and trusted loopback.
8
+ */
9
+ export type AuthContext = {
10
+ kind: 'session';
11
+ orgId: string;
12
+ /** Stable identity, e.g. `github:<numeric id>` — never the login. */
13
+ userRef: string;
14
+ /** better-auth org role (owner/admin/member) for display. */
15
+ role: OrgRole;
16
+ /** Resolved permission set for resource:action checks. */
17
+ permission: PermissionSet;
18
+ } | {
19
+ kind: 'loopback';
20
+ orgId: string;
21
+ /** REQ-21: local loopback single-org is always owner — no login. */
22
+ role: 'owner';
23
+ permission: PermissionSet;
24
+ } | {
25
+ kind: 'apikey';
26
+ orgId: string;
27
+ /** better-auth user id that owns the key (referenceId). */
28
+ userId: string;
29
+ /** Optional project scope from key metadata; cross-project → 404. */
30
+ projectId?: string;
31
+ /**
32
+ * Live member role when present. Effective authority is always
33
+ * `permission` (key ∩ live role; agent profile also strips apiKey —
34
+ * owner profile retains it when live role allows).
35
+ */
36
+ role: OrgRole | undefined;
37
+ permission: PermissionSet;
38
+ } | {
39
+ /** Portal participant after join — no org membership, one share only. */
40
+ kind: 'guest';
41
+ shareId: string;
42
+ projectId: string;
43
+ guestSessionId: string;
44
+ };
45
+ /** Org-bearing contexts (everything except portal guests). */
46
+ export type OrgAuthContext = Exclude<AuthContext, {
47
+ kind: 'guest';
48
+ }>;
49
+ export declare function isOrgAuthContext(ctx: AuthContext): ctx is OrgAuthContext;
50
+ export declare function runWithAuthContext<T>(ctx: AuthContext, fn: () => T): T;
51
+ export declare function getAuthContext(): AuthContext;
52
+ /** Org-bearing auth only — rejects portal guests (no org membership). */
53
+ export declare function getOrgAuthContext(): OrgAuthContext;
54
+ export declare function tryGetAuthContext(): AuthContext | undefined;
55
+ export declare class ReadOnlyTokenError extends Error {
56
+ constructor();
57
+ }
58
+ /** Reject pure read-only callers (empty permission set). Guests never write. */
59
+ export declare function assertWriteAccess(): void;
60
+ //# sourceMappingURL=auth-context.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { hasAnyWritePermission } from './permissions.js';
3
+ export function isOrgAuthContext(ctx) {
4
+ return ctx.kind !== 'guest';
5
+ }
6
+ const storage = new AsyncLocalStorage();
7
+ export function runWithAuthContext(ctx, fn) {
8
+ return storage.run(ctx, fn);
9
+ }
10
+ export function getAuthContext() {
11
+ const ctx = storage.getStore();
12
+ if (ctx === undefined) {
13
+ throw new Error('Auth context is not set — org auth middleware must run first');
14
+ }
15
+ return ctx;
16
+ }
17
+ /** Org-bearing auth only — rejects portal guests (no org membership). */
18
+ export function getOrgAuthContext() {
19
+ const ctx = getAuthContext();
20
+ if (!isOrgAuthContext(ctx)) {
21
+ throw new Error('Guest context has no org membership');
22
+ }
23
+ return ctx;
24
+ }
25
+ export function tryGetAuthContext() {
26
+ return storage.getStore();
27
+ }
28
+ export class ReadOnlyTokenError extends Error {
29
+ constructor() {
30
+ super('read-only token cannot perform write operations');
31
+ this.name = 'ReadOnlyTokenError';
32
+ }
33
+ }
34
+ /** Reject pure read-only callers (empty permission set). Guests never write. */
35
+ export function assertWriteAccess() {
36
+ const ctx = getAuthContext();
37
+ if (ctx.kind === 'guest' || !hasAnyWritePermission(ctx.permission)) {
38
+ throw new ReadOnlyTokenError();
39
+ }
40
+ }
41
+ //# sourceMappingURL=auth-context.js.map
package/dist/auth.d.ts CHANGED
@@ -1,3 +1,36 @@
1
1
  import type { MiddlewareHandler } from 'hono';
2
+ import { type Db } from '@plandesk/db';
3
+ import type { BetterAuthInstance } from './better-auth.js';
4
+ /** True when the server is bound only to loopback (trusted local network boundary). */
5
+ export declare function isLoopbackBind(host: string): boolean;
6
+ export type OrgAuthOptions = {
7
+ db: Db;
8
+ /** Server bind address. Loopback default-org only when this is loopback. */
9
+ bindHost: string;
10
+ /**
11
+ * better-auth instance when configured. Session cookies issued by better-auth
12
+ * are recognized here; omit and only apiKey / loopback / guest apply.
13
+ */
14
+ betterAuth?: BetterAuthInstance;
15
+ };
16
+ export declare function isInvitationAcceptPath(path: string): boolean;
17
+ export declare function isPublicAuthPath(path: string): boolean;
18
+ export declare function isPublicShareReadPath(path: string): boolean;
19
+ export declare function isShareGuestViewPath(path: string): boolean;
20
+ export declare function extractShareTokenFromViewPath(path: string): string | undefined;
21
+ /**
22
+ * Always-on org resolver for every request:
23
+ * 1. Bearer → better-auth API key (BA5 live-role ceiling) when configured
24
+ * 2. else better-auth session cookie → active org + member role (when configured)
25
+ * 3. else loopback bind + single-org → default org as owner (local, zero friction)
26
+ * 4. else 401
27
+ *
28
+ * A stranger bearer that is not a better-auth key is always 401 (no mcp_token fallback).
29
+ * Every branch yields the same `{ orgId, permission }`, so services downstream
30
+ * are identical for a browser, the CLI, and an agent.
31
+ */
32
+ export declare function createOrgAuthMiddleware(options: OrgAuthOptions): MiddlewareHandler;
33
+ /** Optional Basic auth gate (PLANDESK_AUTH_PASSWORD). Skips MCP paths. */
2
34
  export declare function createAuthMiddleware(password: string): MiddlewareHandler;
35
+ export declare function createWriteGuardMiddleware(): MiddlewareHandler;
3
36
  //# sourceMappingURL=auth.d.ts.map