@jungjaehoon/mama-core 2.2.0 → 2.2.1

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,24 @@
1
+ CREATE TABLE IF NOT EXISTS principal_scope_grants (
2
+ principal_id TEXT NOT NULL REFERENCES principals(principal_id),
3
+ grant_kind TEXT NOT NULL CHECK (grant_kind IN ('source', 'memory')),
4
+ scope_kind TEXT NOT NULL CHECK (
5
+ length(trim(scope_kind)) > 0
6
+ AND scope_kind <> '*'
7
+ AND (grant_kind = 'source' OR scope_kind IN ('project', 'channel', 'global'))
8
+ ),
9
+ scope_id TEXT NOT NULL CHECK (length(trim(scope_id)) > 0 AND scope_id <> '*'),
10
+ granted_by_principal_id TEXT NOT NULL REFERENCES principals(principal_id),
11
+ created_at INTEGER NOT NULL,
12
+ revoked_at INTEGER CHECK (revoked_at IS NULL OR revoked_at >= created_at)
13
+ );
14
+
15
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_principal_scope_grants_one_active
16
+ ON principal_scope_grants(principal_id, grant_kind, scope_kind, scope_id)
17
+ WHERE revoked_at IS NULL;
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_principal_scope_grants_active_listing
20
+ ON principal_scope_grants(principal_id, created_at, grant_kind, scope_kind, scope_id)
21
+ WHERE revoked_at IS NULL;
22
+
23
+ INSERT OR IGNORE INTO schema_version (version, description)
24
+ VALUES (65, 'Create principal scope grants for human-team access');
@@ -4,6 +4,29 @@ export interface PrincipalRow {
4
4
  kind: 'owner' | 'member';
5
5
  status: 'active' | 'suspended' | 'offboarded';
6
6
  }
7
+ export declare const SHARED_MEMORY_SCOPE_KINDS: readonly ["project", "channel", "global"];
8
+ export type SharedMemoryScopeKind = (typeof SHARED_MEMORY_SCOPE_KINDS)[number];
9
+ export type PrincipalScopeGrantRef = {
10
+ kind: 'source';
11
+ connector: string;
12
+ channelId: string;
13
+ } | {
14
+ kind: 'memory';
15
+ scopeKind: SharedMemoryScopeKind;
16
+ scopeId: string;
17
+ };
18
+ export interface PrincipalScopeGrantRecord {
19
+ targetPrincipalId: string;
20
+ scope: PrincipalScopeGrantRef;
21
+ grantedByPrincipalId: string;
22
+ createdAt: number;
23
+ }
24
+ export interface PrincipalScopeGrantMutationInput {
25
+ targetPrincipalId: string;
26
+ ownerPrincipalId: string;
27
+ scope: PrincipalScopeGrantRef;
28
+ now: number;
29
+ }
7
30
  export interface PrincipalRepository {
8
31
  resolveByExternal(connector: string, namespace: string, externalId: string): PrincipalRow | null;
9
32
  registerMember(input: {
@@ -27,11 +50,19 @@ export interface PrincipalRepository {
27
50
  displayName?: string;
28
51
  status: string;
29
52
  }>;
53
+ grantScope(input: PrincipalScopeGrantMutationInput): 'created' | 'exists';
54
+ revokeScope(input: PrincipalScopeGrantMutationInput): 'revoked' | 'absent';
55
+ listActiveGrants(principalId: string): PrincipalScopeGrantRecord[];
30
56
  }
31
57
  export type PrincipalRegistrationErrorCode = 'identity_bound_to_owner' | 'member_not_active';
32
58
  export declare class PrincipalRegistrationError extends Error {
33
59
  readonly code: PrincipalRegistrationErrorCode;
34
60
  constructor(code: PrincipalRegistrationErrorCode, message: string);
35
61
  }
62
+ export type PrincipalScopeGrantErrorCode = 'target_not_active_member' | 'grantor_not_active_owner' | 'invalid_scope';
63
+ export declare class PrincipalScopeGrantError extends Error {
64
+ readonly code: PrincipalScopeGrantErrorCode;
65
+ constructor(code: PrincipalScopeGrantErrorCode, message: string);
66
+ }
36
67
  export declare function createPrincipalRepository(adapter: Pick<DBManagerAdapter, 'prepare' | 'transaction'>): PrincipalRepository;
37
68
  //# sourceMappingURL=principal-repository.d.ts.map
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PrincipalRegistrationError = void 0;
3
+ exports.PrincipalScopeGrantError = exports.PrincipalRegistrationError = exports.SHARED_MEMORY_SCOPE_KINDS = void 0;
4
4
  exports.createPrincipalRepository = createPrincipalRepository;
5
5
  const node_crypto_1 = require("node:crypto");
6
+ exports.SHARED_MEMORY_SCOPE_KINDS = ['project', 'channel', 'global'];
6
7
  class PrincipalRegistrationError extends Error {
7
8
  code;
8
9
  constructor(code, message) {
@@ -12,6 +13,90 @@ class PrincipalRegistrationError extends Error {
12
13
  }
13
14
  }
14
15
  exports.PrincipalRegistrationError = PrincipalRegistrationError;
16
+ class PrincipalScopeGrantError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = 'PrincipalScopeGrantError';
21
+ this.code = code;
22
+ }
23
+ }
24
+ exports.PrincipalScopeGrantError = PrincipalScopeGrantError;
25
+ const MAX_SCOPE_COMPONENT_LENGTH = 512;
26
+ function containsControlCharacter(value) {
27
+ return Array.from(value).some((character) => {
28
+ const codePoint = character.codePointAt(0) ?? 0;
29
+ return codePoint < 32 || codePoint === 127;
30
+ });
31
+ }
32
+ function canonicalizeScopeComponent(value, label, lowercase = false) {
33
+ if (typeof value !== 'string') {
34
+ throw new PrincipalScopeGrantError('invalid_scope', `${label} must be a string`);
35
+ }
36
+ const trimmed = value.trim();
37
+ const canonical = lowercase ? trimmed.toLowerCase() : trimmed;
38
+ if (canonical.length === 0 ||
39
+ canonical.length > MAX_SCOPE_COMPONENT_LENGTH ||
40
+ canonical === '*' ||
41
+ containsControlCharacter(canonical)) {
42
+ throw new PrincipalScopeGrantError('invalid_scope', `${label} is not a canonical scope value`);
43
+ }
44
+ return canonical;
45
+ }
46
+ function canonicalizeGrantScope(scope) {
47
+ if (typeof scope !== 'object' || scope === null) {
48
+ throw new PrincipalScopeGrantError('invalid_scope', 'Grant scope must be an object');
49
+ }
50
+ const candidate = scope;
51
+ if (candidate.kind === 'source') {
52
+ return {
53
+ kind: 'source',
54
+ connector: canonicalizeScopeComponent(candidate.connector, 'Source connector', true),
55
+ channelId: canonicalizeScopeComponent(candidate.channelId, 'Source channel ID'),
56
+ };
57
+ }
58
+ if (candidate.kind === 'memory') {
59
+ if (typeof candidate.scopeKind !== 'string' ||
60
+ !exports.SHARED_MEMORY_SCOPE_KINDS.includes(candidate.scopeKind)) {
61
+ throw new PrincipalScopeGrantError('invalid_scope', 'Memory grant kind must be project, channel, or global');
62
+ }
63
+ return {
64
+ kind: 'memory',
65
+ scopeKind: candidate.scopeKind,
66
+ scopeId: canonicalizeScopeComponent(candidate.scopeId, 'Memory scope ID'),
67
+ };
68
+ }
69
+ throw new PrincipalScopeGrantError('invalid_scope', 'Grant kind must be source or memory');
70
+ }
71
+ function scopeColumns(scope) {
72
+ if (scope.kind === 'source') {
73
+ return {
74
+ grantKind: 'source',
75
+ scopeKind: scope.connector,
76
+ scopeId: scope.channelId,
77
+ };
78
+ }
79
+ return {
80
+ grantKind: 'memory',
81
+ scopeKind: scope.scopeKind,
82
+ scopeId: scope.scopeId,
83
+ };
84
+ }
85
+ function mapPrincipalScopeGrant(row) {
86
+ const scope = row.grant_kind === 'source'
87
+ ? { kind: 'source', connector: row.scope_kind, channelId: row.scope_id }
88
+ : {
89
+ kind: 'memory',
90
+ scopeKind: row.scope_kind,
91
+ scopeId: row.scope_id,
92
+ };
93
+ return {
94
+ targetPrincipalId: row.principal_id,
95
+ scope,
96
+ grantedByPrincipalId: row.granted_by_principal_id,
97
+ createdAt: row.created_at,
98
+ };
99
+ }
15
100
  function mintPrincipalId(kind, connector, namespace, externalId) {
16
101
  const identityKey = JSON.stringify([kind, connector, namespace, externalId]);
17
102
  const digest = (0, node_crypto_1.createHash)('sha256').update(identityKey, 'utf8').digest('hex');
@@ -42,6 +127,63 @@ function createPrincipalRepository(adapter) {
42
127
  FROM principals
43
128
  WHERE kind = 'member'
44
129
  ORDER BY created_at ASC, principal_id ASC`);
130
+ const selectActiveMemberStatement = adapter.prepare(`SELECT 1
131
+ FROM principals
132
+ WHERE principal_id = ? AND kind = 'member' AND status = 'active'`);
133
+ const selectActiveGrantorStatement = adapter.prepare(`SELECT 1
134
+ FROM principals
135
+ WHERE principal_id = ? AND kind = 'owner' AND status = 'active'`);
136
+ const insertGrantStatement = adapter.prepare(`INSERT INTO principal_scope_grants (
137
+ principal_id, grant_kind, scope_kind, scope_id,
138
+ granted_by_principal_id, created_at, revoked_at
139
+ )
140
+ SELECT ?, ?, ?, ?, ?, ?, NULL
141
+ WHERE EXISTS (
142
+ SELECT 1 FROM principals
143
+ WHERE principal_id = ? AND kind = 'member' AND status = 'active'
144
+ )
145
+ AND EXISTS (
146
+ SELECT 1 FROM principals
147
+ WHERE principal_id = ? AND kind = 'owner' AND status = 'active'
148
+ )
149
+ ON CONFLICT (principal_id, grant_kind, scope_kind, scope_id)
150
+ WHERE revoked_at IS NULL
151
+ DO NOTHING`);
152
+ const revokeGrantStatement = adapter.prepare(`UPDATE principal_scope_grants
153
+ SET revoked_at = ?
154
+ WHERE principal_id = ? AND grant_kind = ? AND scope_kind = ? AND scope_id = ?
155
+ AND revoked_at IS NULL
156
+ AND EXISTS (
157
+ SELECT 1 FROM principals
158
+ WHERE principal_id = ? AND kind = 'member' AND status = 'active'
159
+ )
160
+ AND EXISTS (
161
+ SELECT 1 FROM principals
162
+ WHERE principal_id = ? AND kind = 'owner' AND status = 'active'
163
+ )`);
164
+ const listActiveGrantsStatement = adapter.prepare(`SELECT
165
+ grants.principal_id,
166
+ grants.grant_kind,
167
+ grants.scope_kind,
168
+ grants.scope_id,
169
+ grants.granted_by_principal_id,
170
+ grants.created_at
171
+ FROM principal_scope_grants AS grants
172
+ JOIN principals AS target ON target.principal_id = grants.principal_id
173
+ WHERE grants.principal_id = ?
174
+ AND grants.revoked_at IS NULL
175
+ AND target.kind = 'member'
176
+ AND target.status = 'active'
177
+ ORDER BY grants.created_at ASC, grants.grant_kind ASC,
178
+ grants.scope_kind ASC, grants.scope_id ASC`);
179
+ function assertGrantMutationPrincipals(targetPrincipalId, ownerPrincipalId) {
180
+ if (!selectActiveMemberStatement.get(targetPrincipalId)) {
181
+ throw new PrincipalScopeGrantError('target_not_active_member', 'Grant target must be an active member principal');
182
+ }
183
+ if (!selectActiveGrantorStatement.get(ownerPrincipalId)) {
184
+ throw new PrincipalScopeGrantError('grantor_not_active_owner', 'Grant mutation requires an active owner principal');
185
+ }
186
+ }
45
187
  function resolveByExternal(connector, namespace, externalId) {
46
188
  const row = resolveByExternalStatement.get(connector, namespace, externalId);
47
189
  return row ? mapPrincipal(row) : null;
@@ -101,6 +243,33 @@ function createPrincipalRepository(adapter) {
101
243
  return row.display_name === null ? member : { ...member, displayName: row.display_name };
102
244
  });
103
245
  }
246
+ function grantScope(input) {
247
+ const canonicalScope = canonicalizeGrantScope(input.scope);
248
+ const { grantKind, scopeKind, scopeId } = scopeColumns(canonicalScope);
249
+ return adapter.transaction(() => {
250
+ const result = insertGrantStatement.run(input.targetPrincipalId, grantKind, scopeKind, scopeId, input.ownerPrincipalId, input.now, input.targetPrincipalId, input.ownerPrincipalId);
251
+ if (result.changes === 1) {
252
+ return 'created';
253
+ }
254
+ assertGrantMutationPrincipals(input.targetPrincipalId, input.ownerPrincipalId);
255
+ return 'exists';
256
+ });
257
+ }
258
+ function revokeScope(input) {
259
+ const canonicalScope = canonicalizeGrantScope(input.scope);
260
+ const { grantKind, scopeKind, scopeId } = scopeColumns(canonicalScope);
261
+ return adapter.transaction(() => {
262
+ const result = revokeGrantStatement.run(input.now, input.targetPrincipalId, grantKind, scopeKind, scopeId, input.targetPrincipalId, input.ownerPrincipalId);
263
+ if (result.changes === 1) {
264
+ return 'revoked';
265
+ }
266
+ assertGrantMutationPrincipals(input.targetPrincipalId, input.ownerPrincipalId);
267
+ return 'absent';
268
+ });
269
+ }
270
+ function listActiveGrants(principalId) {
271
+ return listActiveGrantsStatement.all(principalId).map(mapPrincipalScopeGrant);
272
+ }
104
273
  return {
105
274
  resolveByExternal,
106
275
  registerMember,
@@ -113,6 +282,9 @@ function createPrincipalRepository(adapter) {
113
282
  },
114
283
  ensureOwner,
115
284
  listMembers,
285
+ grantScope,
286
+ revokeScope,
287
+ listActiveGrants,
116
288
  };
117
289
  }
118
290
  //# sourceMappingURL=principal-repository.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-core",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "MAMA Core - Shared modules for Memory-Augmented MCP Assistant",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",