@kubun/server 0.4.1 → 0.4.3

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.
@@ -0,0 +1,46 @@
1
+ import type { KubunDB } from '@kubun/db';
2
+ import type { DocumentNode } from '@kubun/protocol';
3
+ export type AccessRule = {
4
+ level: string;
5
+ allowedDIDs: Array<string> | null;
6
+ };
7
+ export type AccessPermissions = {
8
+ read?: AccessRule;
9
+ write?: AccessRule;
10
+ };
11
+ export type ServerAccessConfig = {
12
+ defaultAccessLevel: {
13
+ read: 'only_owner' | 'anyone' | 'allowed_dids';
14
+ write: 'only_owner' | 'allowed_dids';
15
+ };
16
+ };
17
+ export type AccessChecker = (doc: DocumentNode, permissionType: 'read' | 'write') => Promise<boolean>;
18
+ /**
19
+ * Parse and validate access permissions from document data
20
+ */
21
+ export declare function parseDocumentAccessPermissions(data: any): AccessPermissions | null;
22
+ /**
23
+ * Validate that DIDs have the correct format
24
+ */
25
+ export declare function validateDIDs(dids: Array<string>): void;
26
+ /**
27
+ * Resolve the effective access rule for a document and permission type
28
+ * Order of precedence:
29
+ * 1. Document accessPermissions override
30
+ * 2. User's model default from database
31
+ * 3. Server configuration default
32
+ */
33
+ export declare function resolveAccessRule(document: DocumentNode, modelId: string, ownerDID: string, permissionType: 'read' | 'write', db: KubunDB, serverConfig: ServerAccessConfig): Promise<AccessRule>;
34
+ /**
35
+ * Check if viewer has access to document through delegation tokens
36
+ */
37
+ export declare function checkDelegation(viewerDID: string, grantor: string, document: DocumentNode, permissionType: 'read' | 'write', delegationTokens?: Array<string>): Promise<boolean>;
38
+ /**
39
+ * Check if viewer has access to a document for the specified permission type
40
+ */
41
+ export declare function checkAccess(viewerDID: string | null, document: DocumentNode, permissionType: 'read' | 'write', db: KubunDB, serverConfig: ServerAccessConfig, delegationTokens?: Array<string>): Promise<boolean>;
42
+ /**
43
+ * Create an access checker function bound to specific viewer and delegation tokens
44
+ */
45
+ export declare function createAccessChecker(viewerDID: string | null, delegationTokens: Array<string> | undefined, db: KubunDB, serverConfig: ServerAccessConfig): AccessChecker;
46
+ //# sourceMappingURL=access-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/data/access-control.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,kBAAkB,EAAE;QAClB,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,cAAc,CAAA;QAC9C,KAAK,EAAE,YAAY,GAAG,cAAc,CAAA;KACrC,CAAA;CACF,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,YAAY,EACjB,cAAc,EAAE,MAAM,GAAG,OAAO,KAC7B,OAAO,CAAC,OAAO,CAAC,CAAA;AAErB;;GAEG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,GAAG,GAAG,iBAAiB,GAAG,IAAI,CAiBlF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAMtD;AAaD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GAAG,OAAO,EAChC,EAAE,EAAE,OAAO,EACX,YAAY,EAAE,kBAAkB,GAC/B,OAAO,CAAC,UAAU,CAAC,CA4BrB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,YAAY,EACtB,cAAc,EAAE,MAAM,GAAG,OAAO,EAChC,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAC/B,OAAO,CAAC,OAAO,CAAC,CAoClB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,YAAY,EACtB,cAAc,EAAE,MAAM,GAAG,OAAO,EAChC,EAAE,EAAE,OAAO,EACX,YAAY,EAAE,kBAAkB,EAChC,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAC/B,OAAO,CAAC,OAAO,CAAC,CA+DlB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,gBAAgB,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,EAC3C,EAAE,EAAE,OAAO,EACX,YAAY,EAAE,kBAAkB,GAC/B,aAAa,CAIf"}
@@ -0,0 +1,164 @@
1
+ import { checkCapability } from '@enkaku/capability';
2
+ /**
3
+ * Parse and validate access permissions from document data
4
+ */ export function parseDocumentAccessPermissions(data) {
5
+ try {
6
+ if (!data.accessPermissions) return null;
7
+ const perms = data.accessPermissions;
8
+ // Basic validation - check if the permission object has the expected structure
9
+ if (typeof perms !== 'object') {
10
+ console.warn('Invalid accessPermissions structure in document, ignoring');
11
+ return null;
12
+ }
13
+ return perms;
14
+ } catch (error) {
15
+ console.warn('Failed to parse document access permissions:', error);
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * Validate that DIDs have the correct format
21
+ */ export function validateDIDs(dids) {
22
+ for (const did of dids){
23
+ if (!did.startsWith('did:')) {
24
+ throw new Error(`Invalid DID format: ${did}`);
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Check if a level string is valid for the given permission type
30
+ */ function isValidAccessLevel(level, permissionType) {
31
+ if (permissionType === 'read') {
32
+ return level === 'only_owner' || level === 'anyone' || level === 'allowed_dids';
33
+ }
34
+ // write permission
35
+ return level === 'only_owner' || level === 'allowed_dids';
36
+ }
37
+ /**
38
+ * Resolve the effective access rule for a document and permission type
39
+ * Order of precedence:
40
+ * 1. Document accessPermissions override
41
+ * 2. User's model default from database
42
+ * 3. Server configuration default
43
+ */ export async function resolveAccessRule(document, modelId, ownerDID, permissionType, db, serverConfig) {
44
+ // 1. Check document override
45
+ const docPerms = parseDocumentAccessPermissions(document.data);
46
+ if (docPerms?.[permissionType]) {
47
+ const rule = docPerms[permissionType];
48
+ if (rule && isValidAccessLevel(rule.level, permissionType)) {
49
+ return {
50
+ level: rule.level,
51
+ allowedDIDs: rule.allowedDIDs || null
52
+ };
53
+ }
54
+ }
55
+ // 2. Check user's model default
56
+ const modelDefault = await db.getUserModelAccessDefault(ownerDID, modelId, permissionType);
57
+ if (modelDefault) {
58
+ return {
59
+ level: modelDefault.level,
60
+ allowedDIDs: modelDefault.allowedDIDs
61
+ };
62
+ }
63
+ // 3. Fall back to server default
64
+ const level = serverConfig.defaultAccessLevel[permissionType];
65
+ return {
66
+ level,
67
+ allowedDIDs: null
68
+ };
69
+ }
70
+ /**
71
+ * Check if viewer has access to document through delegation tokens
72
+ */ export async function checkDelegation(viewerDID, grantor, document, permissionType, delegationTokens) {
73
+ if (!delegationTokens || delegationTokens.length === 0) {
74
+ return false;
75
+ }
76
+ // Build expected permission based on specificity
77
+ const resources = [
78
+ `urn:kubun:document:${document.id}`,
79
+ `urn:kubun:model:${document.model}`,
80
+ '*'
81
+ ];
82
+ const action = `document/${permissionType}` // document/read or document/write
83
+ ;
84
+ // First, try tokens as a delegation chain (for A→B→C scenarios)
85
+ for (const res of resources){
86
+ try {
87
+ await checkCapability({
88
+ act: action,
89
+ res
90
+ }, {
91
+ iss: viewerDID,
92
+ sub: grantor,
93
+ cap: delegationTokens
94
+ });
95
+ return true;
96
+ } catch {}
97
+ }
98
+ // If chain validation fails, try each token independently (for multiple independent grants)
99
+ for (const token of delegationTokens){
100
+ for (const res of resources){
101
+ try {
102
+ await checkCapability({
103
+ act: action,
104
+ res
105
+ }, {
106
+ iss: viewerDID,
107
+ sub: grantor,
108
+ cap: token
109
+ });
110
+ return true;
111
+ } catch {}
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ /**
117
+ * Check if viewer has access to a document for the specified permission type
118
+ */ export async function checkAccess(viewerDID, document, permissionType, db, serverConfig, delegationTokens) {
119
+ // Validate document has owner
120
+ if (!document.owner) {
121
+ throw new Error('Document missing owner field');
122
+ }
123
+ // FAST PATH: Owner always has access
124
+ if (viewerDID === document.owner) {
125
+ return true;
126
+ }
127
+ // Resolve effective access rule
128
+ const rule = await resolveAccessRule(document, document.model, document.owner, permissionType, db, serverConfig);
129
+ // ANYONE: Always allow (read only)
130
+ if (rule.level === 'anyone') {
131
+ return true;
132
+ }
133
+ // No viewer: Deny
134
+ if (!viewerDID) {
135
+ return false;
136
+ }
137
+ // ONLY_OWNER: Check delegation from owner
138
+ if (rule.level === 'only_owner') {
139
+ return await checkDelegation(viewerDID, document.owner, document, permissionType, delegationTokens);
140
+ }
141
+ // ALLOWED_DIDS: Check if viewer is in list or has delegation
142
+ if (rule.level === 'allowed_dids') {
143
+ const allowedDIDs = rule.allowedDIDs || [];
144
+ if (allowedDIDs.includes(viewerDID)) {
145
+ return true;
146
+ }
147
+ // Check if any allowedDID has delegated to viewer
148
+ for (const allowedDID of allowedDIDs){
149
+ if (await checkDelegation(viewerDID, allowedDID, document, permissionType, delegationTokens)) {
150
+ return true;
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ // Unknown access level
156
+ return false;
157
+ }
158
+ /**
159
+ * Create an access checker function bound to specific viewer and delegation tokens
160
+ */ export function createAccessChecker(viewerDID, delegationTokens, db, serverConfig) {
161
+ return async (doc, permissionType)=>{
162
+ return await checkAccess(viewerDID, doc, permissionType, db, serverConfig, delegationTokens);
163
+ };
164
+ }
@@ -2,10 +2,12 @@ import type { KubunDB } from '@kubun/db';
2
2
  import { type Context } from '@kubun/graphql';
3
3
  import type { DocumentNode } from '@kubun/protocol';
4
4
  import { type ExecutionArgs, type GraphQLSchema, type OperationTypeNode } from 'graphql';
5
+ import type { AccessChecker } from './access-control.js';
5
6
  export type ExecutionContext = {
6
7
  db: KubunDB;
7
8
  viewerDID: string;
8
9
  mutatedDocuments?: Record<string, DocumentNode>;
10
+ accessChecker?: AccessChecker;
9
11
  };
10
12
  export declare function createContext(ctx: ExecutionContext): Context;
11
13
  export type ExecuteGraphQLParams = {
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/data/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,KAAK,OAAO,EAA+C,MAAM,gBAAgB,CAAA;AAC1F,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,KAAK,iBAAiB,EAEvB,MAAM,SAAS,CAAA;AAEhB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,OAAO,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CAChD,CAAA;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAkC5D;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,aAAa,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,iBAAiB,CAAA;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAiB5E"}
1
+ {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/data/graphql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,KAAK,OAAO,EAA+C,MAAM,gBAAgB,CAAA;AAC1F,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,KAAK,iBAAiB,EAEvB,MAAM,SAAS,CAAA;AAEhB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAQxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,OAAO,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAC/C,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B,CAAA;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAgG5D;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,aAAa,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,iBAAiB,CAAA;IACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAiB5E"}
@@ -1,7 +1,12 @@
1
1
  import { createReadContext } from '@kubun/graphql';
2
2
  import { Kind, parse } from 'graphql';
3
+ import { removeDocumentAccessOverride, removeModelAccessDefaults, setDocumentAccessOverride, setModelAccessDefaults } from './mutations.js';
3
4
  export function createContext(ctx) {
4
- const readContext = createReadContext(ctx);
5
+ const readContext = createReadContext({
6
+ db: ctx.db,
7
+ viewerDID: ctx.viewerDID,
8
+ accessChecker: ctx.accessChecker
9
+ });
5
10
  function getMutationDocument(info) {
6
11
  return ctx.mutatedDocuments?.[info.path.key];
7
12
  }
@@ -18,6 +23,36 @@ export function createContext(ctx) {
18
23
  },
19
24
  async executeRemoveMutation (_id, _info) {
20
25
  // no-op
26
+ },
27
+ async executeSetModelAccessDefaults (modelId, permissionType, accessLevel, allowedDIDs) {
28
+ await setModelAccessDefaults({
29
+ ownerDID: ctx.viewerDID,
30
+ modelID: modelId,
31
+ permissionType,
32
+ accessLevel,
33
+ allowedDIDs
34
+ }, ctx.db);
35
+ },
36
+ async executeRemoveModelAccessDefaults (modelId, permissionTypes) {
37
+ await removeModelAccessDefaults({
38
+ ownerDID: ctx.viewerDID,
39
+ modelID: modelId,
40
+ permissionTypes
41
+ }, ctx.db);
42
+ },
43
+ async executeSetDocumentAccessOverride (documentId, permissionType, accessLevel, allowedDIDs) {
44
+ await setDocumentAccessOverride({
45
+ documentID: documentId,
46
+ permissionType,
47
+ accessLevel,
48
+ allowedDIDs
49
+ }, ctx.db);
50
+ },
51
+ async executeRemoveDocumentAccessOverride (documentId, permissionTypes) {
52
+ await removeDocumentAccessOverride({
53
+ documentID: documentId,
54
+ permissionTypes
55
+ }, ctx.db);
21
56
  }
22
57
  };
23
58
  }
@@ -1,4 +1,41 @@
1
+ import type { KubunDB } from '@kubun/db';
1
2
  import { type MutationContext } from '@kubun/mutation';
2
3
  import { type DocumentNode } from '@kubun/protocol';
4
+ import { type ServerAccessConfig } from './access-control.js';
3
5
  export declare function applyMutation(ctx: MutationContext, token: string): Promise<DocumentNode>;
6
+ /**
7
+ * Set model-level access defaults for a user
8
+ */
9
+ export declare function setModelAccessDefaults(params: {
10
+ ownerDID: string;
11
+ modelID: string;
12
+ permissionType: 'read' | 'write';
13
+ accessLevel: string;
14
+ allowedDIDs: Array<string> | null;
15
+ }, db: KubunDB): Promise<void>;
16
+ /**
17
+ * Remove model-level access defaults for a user
18
+ */
19
+ export declare function removeModelAccessDefaults(params: {
20
+ ownerDID: string;
21
+ modelID: string;
22
+ permissionTypes: Array<'read' | 'write'>;
23
+ }, db: KubunDB): Promise<void>;
24
+ /**
25
+ * Set document-level access override
26
+ */
27
+ export declare function setDocumentAccessOverride(params: {
28
+ documentID: string;
29
+ permissionType: 'read' | 'write';
30
+ accessLevel: string;
31
+ allowedDIDs: Array<string> | null;
32
+ }, db: KubunDB): Promise<void>;
33
+ /**
34
+ * Remove document-level access override
35
+ */
36
+ export declare function removeDocumentAccessOverride(params: {
37
+ documentID: string;
38
+ permissionTypes: Array<'read' | 'write'>;
39
+ }, db: KubunDB): Promise<void>;
40
+ export type { ServerAccessConfig };
4
41
  //# sourceMappingURL=mutations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../../src/data/mutations.ts"],"names":[],"mappings":"AAEA,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC9E,OAAO,EAAyB,KAAK,YAAY,EAAoB,MAAM,iBAAiB,CAAA;AAI5F,wBAAsB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAI9F"}
1
+ {"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../../src/data/mutations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC9E,OAAO,EAAyB,KAAK,YAAY,EAAoB,MAAM,iBAAiB,CAAA;AAE5F,OAAO,EAAE,KAAK,kBAAkB,EAAgB,MAAM,qBAAqB,CAAA;AAI3E,wBAAsB,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAI9F;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE;IACN,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;CAClC,EACD,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE;IACN,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,CAAA;CACzC,EACD,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE;IACN,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;CAClC,EACD,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED;;GAEG;AACH,wBAAsB,4BAA4B,CAChD,MAAM,EAAE;IACN,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,CAAA;CACzC,EACD,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAGD,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
@@ -1,10 +1,114 @@
1
1
  import { asType, createValidator } from '@enkaku/schema';
2
2
  import { verifyToken } from '@enkaku/token';
3
+ import { DocumentID } from '@kubun/id';
3
4
  import { applyMutation as apply } from '@kubun/mutation';
4
5
  import { documentMutation } from '@kubun/protocol';
6
+ import { validateDIDs } from './access-control.js';
5
7
  const validateMutation = createValidator(documentMutation);
6
8
  export async function applyMutation(ctx, token) {
7
9
  const verified = await verifyToken(token);
8
10
  const mutation = asType(validateMutation, verified.payload);
9
11
  return await apply(ctx, mutation);
10
12
  }
13
+ /**
14
+ * Set model-level access defaults for a user
15
+ */ export async function setModelAccessDefaults(params, db) {
16
+ const { ownerDID, modelID, permissionType, accessLevel, allowedDIDs } = params;
17
+ // Validate DIDs if provided
18
+ if (allowedDIDs && allowedDIDs.length > 0) {
19
+ validateDIDs(allowedDIDs);
20
+ }
21
+ // Validate access level for permission type
22
+ const validLevels = {
23
+ read: [
24
+ 'only_owner',
25
+ 'anyone',
26
+ 'allowed_dids'
27
+ ],
28
+ write: [
29
+ 'only_owner',
30
+ 'allowed_dids'
31
+ ]
32
+ };
33
+ if (!validLevels[permissionType].includes(accessLevel)) {
34
+ throw new Error(`Invalid access level "${accessLevel}" for permission type "${permissionType}"`);
35
+ }
36
+ await db.setUserModelAccessDefault({
37
+ ownerDID,
38
+ modelID,
39
+ permissionType,
40
+ accessLevel,
41
+ allowedDIDs
42
+ });
43
+ }
44
+ /**
45
+ * Remove model-level access defaults for a user
46
+ */ export async function removeModelAccessDefaults(params, db) {
47
+ const { ownerDID, modelID, permissionTypes } = params;
48
+ await db.removeUserModelAccessDefaults(ownerDID, modelID, permissionTypes);
49
+ }
50
+ /**
51
+ * Set document-level access override
52
+ */ export async function setDocumentAccessOverride(params, db) {
53
+ const { documentID, permissionType, accessLevel, allowedDIDs } = params;
54
+ // Validate DIDs if provided
55
+ if (allowedDIDs && allowedDIDs.length > 0) {
56
+ validateDIDs(allowedDIDs);
57
+ }
58
+ // Get the document
59
+ const docID = DocumentID.fromString(documentID);
60
+ const doc = await db.getDocument(docID);
61
+ if (!doc) {
62
+ throw new Error(`Document not found: ${documentID}`);
63
+ }
64
+ // Preserve existing permissions
65
+ const existingPerms = doc.data?.accessPermissions || {};
66
+ // Update the specific permission type
67
+ const updatedPerms = {
68
+ ...existingPerms,
69
+ [permissionType]: {
70
+ level: accessLevel,
71
+ allowedDIDs
72
+ }
73
+ };
74
+ // Update the document with new access permissions
75
+ await db.saveDocument({
76
+ id: docID,
77
+ data: {
78
+ ...doc.data,
79
+ accessPermissions: updatedPerms
80
+ },
81
+ state: null,
82
+ existing: doc
83
+ });
84
+ }
85
+ /**
86
+ * Remove document-level access override
87
+ */ export async function removeDocumentAccessOverride(params, db) {
88
+ const { documentID, permissionTypes } = params;
89
+ // Get the document
90
+ const docID = DocumentID.fromString(documentID);
91
+ const doc = await db.getDocument(docID);
92
+ if (!doc) {
93
+ // Silently return if document doesn't exist
94
+ return;
95
+ }
96
+ // Preserve existing permissions, removing specified types
97
+ const existingPerms = doc.data?.accessPermissions || {};
98
+ const updatedPerms = {
99
+ ...existingPerms
100
+ };
101
+ for (const permType of permissionTypes){
102
+ delete updatedPerms[permType];
103
+ }
104
+ // Update the document with updated access permissions
105
+ await db.saveDocument({
106
+ id: docID,
107
+ data: {
108
+ ...doc.data,
109
+ accessPermissions: Object.keys(updatedPerms).length > 0 ? updatedPerms : undefined
110
+ },
111
+ state: null,
112
+ existing: doc
113
+ });
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/handlers/graph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAKvD,OAAO,KAAK,EAKV,aAAa,EACd,MAAM,iBAAiB,CAAA;AAcxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAUtD,wBAAgB,cAAc,CAC5B,cAAc,EAAE,oBAAoB,GACnC,iBAAiB,CAAC,aAAa,CAAC,CAsJlC"}
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/handlers/graph.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAKvD,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,iBAAiB,CAAA;AAexB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAUtD,wBAAgB,cAAc,CAC5B,cAAc,EAAE,oBAAoB,GACnC,iBAAiB,CAAC,aAAa,CAAC,CAqKlC"}
@@ -5,6 +5,7 @@ import { AttachmentID } from '@kubun/id';
5
5
  import { GraphModel } from '@kubun/protocol';
6
6
  import { execute, OperationTypeNode, // printSchema,
7
7
  subscribe } from 'graphql';
8
+ import { createAccessChecker } from '../data/access-control.js';
8
9
  import { getExecutionArgs } from '../data/graphql.js';
9
10
  import { applyMutation } from '../data/mutations.js';
10
11
  function toGraphResult(result) {
@@ -15,7 +16,7 @@ function toGraphResult(result) {
15
16
  };
16
17
  }
17
18
  export function createHandlers(handlersParams) {
18
- const { db, logger } = handlersParams;
19
+ const { db, logger, serverAccessConfig } = handlersParams;
19
20
  const graphModels = {};
20
21
  async function getGraphModels(id) {
21
22
  if (graphModels[id] == null) {
@@ -30,7 +31,10 @@ export function createHandlers(handlersParams) {
30
31
  logger.debug('cached model for graph {id}', {
31
32
  id
32
33
  });
33
- return graph.record;
34
+ return {
35
+ record: graph.record,
36
+ aliases: graph.aliases
37
+ };
34
38
  });
35
39
  }
36
40
  return await graphModels[id];
@@ -38,8 +42,8 @@ export function createHandlers(handlersParams) {
38
42
  const schemas = {};
39
43
  async function getGraphQLSchema(id) {
40
44
  if (schemas[id] == null) {
41
- schemas[id] = getGraphModels(id).then((record)=>{
42
- const schema = createSchema(record);
45
+ schemas[id] = getGraphModels(id).then((model)=>{
46
+ const schema = createSchema(model);
43
47
  logger.debug('cached schema for graph {id}', {
44
48
  id
45
49
  });
@@ -71,7 +75,7 @@ export function createHandlers(handlersParams) {
71
75
  });
72
76
  return {
73
77
  id,
74
- models: model.record
78
+ ...model.toJSON()
75
79
  };
76
80
  },
77
81
  'graph/list': async ()=>{
@@ -84,9 +88,7 @@ export function createHandlers(handlersParams) {
84
88
  };
85
89
  },
86
90
  'graph/load': async (ctx)=>{
87
- return {
88
- models: await getGraphModels(ctx.param.id)
89
- };
91
+ return await getGraphModels(ctx.param.id);
90
92
  },
91
93
  'graph/mutate': async (ctx)=>{
92
94
  const attachments = Object.entries(ctx.param.attachments ?? {}).map(([key, value])=>{
@@ -109,9 +111,11 @@ export function createHandlers(handlersParams) {
109
111
  }, mutation);
110
112
  }));
111
113
  const payload = ctx.message.payload;
112
- // TODO: viewerDID is token subject or fallback to the issuer
113
- // also add token capabilities check
114
- const viewerDID = payload.iss;
114
+ const viewerDID = payload.sub || payload.iss;
115
+ const delegationTokens = payload.cap ? Array.isArray(payload.cap) ? payload.cap : [
116
+ payload.cap
117
+ ] : undefined;
118
+ const accessChecker = createAccessChecker(viewerDID, delegationTokens, db, serverAccessConfig);
115
119
  return await executeGraphQL({
116
120
  schema: await getGraphQLSchema(ctx.param.id),
117
121
  type: OperationTypeNode.MUTATION,
@@ -120,15 +124,18 @@ export function createHandlers(handlersParams) {
120
124
  context: {
121
125
  db,
122
126
  mutatedDocuments,
123
- viewerDID
127
+ viewerDID,
128
+ accessChecker
124
129
  }
125
130
  });
126
131
  },
127
132
  'graph/query': async (ctx)=>{
128
133
  const payload = ctx.message.payload;
129
- // TODO: viewerDID is token subject or fallback to the issuer
130
- // also add token capabilities check
131
- const viewerDID = payload.iss;
134
+ const viewerDID = payload.sub || payload.iss;
135
+ const delegationTokens = payload.cap ? Array.isArray(payload.cap) ? payload.cap : [
136
+ payload.cap
137
+ ] : undefined;
138
+ const accessChecker = createAccessChecker(viewerDID, delegationTokens, db, serverAccessConfig);
132
139
  return await executeGraphQL({
133
140
  schema: await getGraphQLSchema(ctx.param.id),
134
141
  type: OperationTypeNode.QUERY,
@@ -136,13 +143,18 @@ export function createHandlers(handlersParams) {
136
143
  variables: ctx.param.variables ?? {},
137
144
  context: {
138
145
  db,
139
- viewerDID
146
+ viewerDID,
147
+ accessChecker
140
148
  }
141
149
  });
142
150
  },
143
151
  'graph/subscribe': async (ctx)=>{
144
152
  const payload = ctx.message.payload;
145
- const viewerDID = payload.iss;
153
+ const viewerDID = payload.sub || payload.iss;
154
+ const delegationTokens = payload.cap ? Array.isArray(payload.cap) ? payload.cap : [
155
+ payload.cap
156
+ ] : undefined;
157
+ const accessChecker = createAccessChecker(viewerDID, delegationTokens, db, serverAccessConfig);
146
158
  const args = getExecutionArgs({
147
159
  schema: await getGraphQLSchema(ctx.param.id),
148
160
  type: OperationTypeNode.SUBSCRIPTION,
@@ -150,7 +162,8 @@ export function createHandlers(handlersParams) {
150
162
  variables: ctx.param.variables ?? {},
151
163
  context: {
152
164
  db,
153
- viewerDID
165
+ viewerDID,
166
+ accessChecker
154
167
  }
155
168
  });
156
169
  const subscription = await subscribe(args);
@@ -1,7 +1,9 @@
1
1
  import type { KubunDB } from '@kubun/db';
2
2
  import type { Logger } from '@kubun/logger';
3
+ import type { ServerAccessConfig } from '../data/access-control.js';
3
4
  export type CreateHandlersParams = {
4
5
  db: KubunDB;
5
6
  logger: Logger;
7
+ serverAccessConfig: ServerAccessConfig;
6
8
  };
7
9
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/handlers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/handlers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAEnE,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,kBAAkB,EAAE,kBAAkB,CAAA;CACvC,CAAA"}
package/lib/server.d.ts CHANGED
@@ -8,6 +8,10 @@ export type ServerParams = {
8
8
  db: KubunDB | DBParams;
9
9
  id: string;
10
10
  logger?: Logger;
11
+ defaultAccessLevel?: {
12
+ read?: 'only_owner' | 'anyone' | 'allowed_dids';
13
+ write?: 'only_owner' | 'allowed_dids';
14
+ };
11
15
  };
12
16
  export type CreateClientParams = Omit<ClientParams, 'serverID' | 'transport'> & {
13
17
  signal?: AbortSignal;
@@ -16,6 +20,10 @@ export declare class KubunServer {
16
20
  #private;
17
21
  constructor(params: ServerParams);
18
22
  get db(): KubunDB;
23
+ get defaultAccessLevel(): {
24
+ read: "only_owner" | "anyone" | "allowed_dids";
25
+ write: "only_owner" | "allowed_dids";
26
+ };
19
27
  createClient(params: CreateClientParams): KubunClient;
20
28
  serve(transport: ServerTransport, signal?: AbortSignal): Server<Protocol>;
21
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAS,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,KAAK,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAiB,QAAQ,EAAiB,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAI9F,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,EAAE,EAAE,OAAO,GAAG,QAAQ,CAAA;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,WAAW,CAAC,GAAG;IAC9E,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,CAAA;AAED,qBAAa,WAAW;;gBAOV,MAAM,EAAE,YAAY;IAUhC,IAAI,EAAE,IAAI,OAAO,CAEhB;IAED,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW;IAkBrD,KAAK,CAAC,SAAS,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC;CAU1E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAS,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,KAAK,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAAiB,QAAQ,EAAiB,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAI9F,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,EAAE,EAAE,OAAO,GAAG,QAAQ,CAAA;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE;QACnB,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ,GAAG,cAAc,CAAA;QAC/C,KAAK,CAAC,EAAE,YAAY,GAAG,cAAc,CAAA;KACtC,CAAA;CACF,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,WAAW,CAAC,GAAG;IAC9E,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,CAAA;AAED,qBAAa,WAAW;;gBAWV,MAAM,EAAE,YAAY;IAmBhC,IAAI,EAAE,IAAI,OAAO,CAEhB;IAED,IAAI,kBAAkB;cA3Bd,YAAY,GAAG,QAAQ,GAAG,cAAc;eACvC,YAAY,GAAG,cAAc;MA4BrC;IAED,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW;IAkBrD,KAAK,CAAC,SAAS,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC;CAU1E"}
package/lib/server.js CHANGED
@@ -10,6 +10,7 @@ export class KubunServer {
10
10
  #handlers;
11
11
  #id;
12
12
  #logger;
13
+ #defaultAccessLevel;
13
14
  constructor(params){
14
15
  const { access, db, id } = params;
15
16
  const logger = params.logger ?? getKubunLogger('server', {
@@ -17,16 +18,27 @@ export class KubunServer {
17
18
  });
18
19
  this.#access = access ?? {};
19
20
  this.#db = db instanceof KubunDB ? db : new KubunDB(db);
21
+ this.#id = id;
22
+ this.#logger = logger;
23
+ // Secure by default
24
+ this.#defaultAccessLevel = {
25
+ read: params.defaultAccessLevel?.read ?? 'only_owner',
26
+ write: params.defaultAccessLevel?.write ?? 'only_owner'
27
+ };
20
28
  this.#handlers = createHandlers({
21
29
  db: this.#db,
22
- logger
30
+ logger,
31
+ serverAccessConfig: {
32
+ defaultAccessLevel: this.#defaultAccessLevel
33
+ }
23
34
  });
24
- this.#id = id;
25
- this.#logger = logger;
26
35
  }
27
36
  get db() {
28
37
  return this.#db;
29
38
  }
39
+ get defaultAccessLevel() {
40
+ return this.#defaultAccessLevel;
41
+ }
30
42
  createClient(params) {
31
43
  const { signal, ...clientParams } = params;
32
44
  const transports = new DirectTransports({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/server",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -16,26 +16,28 @@
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
18
  "@enkaku/async": "^0.12.2",
19
+ "@enkaku/capability": "^0.12.1",
19
20
  "@enkaku/codec": "^0.12.0",
20
21
  "@enkaku/generator": "^0.12.1",
21
22
  "@enkaku/schema": "^0.12.1",
22
- "@enkaku/server": "^0.12.2",
23
+ "@enkaku/server": "^0.12.3",
23
24
  "@enkaku/token": "0.12.3",
24
25
  "@enkaku/transport": "0.12.0",
25
26
  "graphql": "^16.12.0",
26
- "@kubun/db": "^0.4.0",
27
- "@kubun/id": "^0.4.0",
28
- "@kubun/protocol": "^0.4.0",
29
- "@kubun/client": "^0.4.0",
30
- "@kubun/graphql": "^0.4.1",
27
+ "@kubun/client": "^0.4.1",
31
28
  "@kubun/mutation": "^0.4.0",
32
- "@kubun/logger": "^0.4.0"
29
+ "@kubun/graphql": "^0.4.5",
30
+ "@kubun/logger": "^0.4.0",
31
+ "@kubun/protocol": "^0.4.1",
32
+ "@kubun/db": "^0.4.0",
33
+ "@kubun/id": "^0.4.0"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@databases/pg-test": "^3.1.2",
36
37
  "@enkaku/stream": "^0.12.1",
37
- "@kubun/scalars": "^0.4.0",
38
38
  "@kubun/db-postgres": "^0.4.0",
39
+ "@kubun/db-sqlite": "^0.4.0",
40
+ "@kubun/scalars": "^0.4.0",
39
41
  "@kubun/test-utils": "^0.4.0"
40
42
  },
41
43
  "scripts": {