@jungjaehoon/mama-core 2.1.2 → 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.
- package/db/migrations/064-create-principals.sql +27 -0
- package/db/migrations/065-create-principal-scope-grants.sql +24 -0
- package/dist/embedding-server/index.js +6 -6
- package/dist/identity/principal-repository.d.ts +68 -0
- package/dist/identity/principal-repository.js +290 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS principals (
|
|
2
|
+
principal_id TEXT PRIMARY KEY,
|
|
3
|
+
kind TEXT NOT NULL CHECK (kind IN ('owner', 'member')),
|
|
4
|
+
display_name TEXT,
|
|
5
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'offboarded')),
|
|
6
|
+
created_at INTEGER NOT NULL,
|
|
7
|
+
updated_at INTEGER NOT NULL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_principals_at_most_one_owner
|
|
11
|
+
ON principals(kind)
|
|
12
|
+
WHERE kind = 'owner' AND status = 'active';
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS external_identities (
|
|
15
|
+
connector TEXT NOT NULL,
|
|
16
|
+
namespace TEXT NOT NULL,
|
|
17
|
+
external_id TEXT NOT NULL,
|
|
18
|
+
principal_id TEXT NOT NULL REFERENCES principals(principal_id),
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
PRIMARY KEY (connector, namespace, external_id)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_external_identities_principal
|
|
24
|
+
ON external_identities(principal_id);
|
|
25
|
+
|
|
26
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
27
|
+
VALUES (64, 'Create principals and external_identities for member registry');
|
|
@@ -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');
|
|
@@ -32,9 +32,9 @@ exports.SHUTDOWN_TOKEN = process.env.MAMA_SHUTDOWN_TOKEN || crypto_1.default.ran
|
|
|
32
32
|
const embeddings_js_1 = require("../embeddings.js");
|
|
33
33
|
const config_loader_js_1 = require("../config-loader.js");
|
|
34
34
|
const db_manager_js_1 = require("../db-manager.js");
|
|
35
|
-
// Import Session API handler (
|
|
35
|
+
// Import Session API handler (chat session API)
|
|
36
36
|
const session_api_js_1 = require("./mobile/session-api.js");
|
|
37
|
-
// Import WebSocket handler (
|
|
37
|
+
// Import WebSocket handler (chat WebSocket API - V2 with MessageRouter)
|
|
38
38
|
const websocket_handler_js_1 = require("./mobile/websocket-handler.js");
|
|
39
39
|
// Import Session Manager
|
|
40
40
|
const session_manager_js_1 = require("./mobile/session-manager.js");
|
|
@@ -95,7 +95,7 @@ async function handleRequest(req, res) {
|
|
|
95
95
|
// Security Note: CORS '*' is safe here because:
|
|
96
96
|
// 1. Server binds to localhost only (127.0.0.1)
|
|
97
97
|
// 2. No sensitive data exposed (user's own decisions)
|
|
98
|
-
// 3. Required for browser-based
|
|
98
|
+
// 3. Required for the browser-based Viewer
|
|
99
99
|
res.setHeader('Content-Type', 'application/json');
|
|
100
100
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
101
101
|
// Health check
|
|
@@ -212,7 +212,7 @@ async function handleRequest(req, res) {
|
|
|
212
212
|
}
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
|
-
// Session API routes (
|
|
215
|
+
// Session API routes (chat session API)
|
|
216
216
|
try {
|
|
217
217
|
const sessionHandled = await sessionHandler(req, res);
|
|
218
218
|
if (sessionHandled) {
|
|
@@ -257,7 +257,7 @@ async function handleRequest(req, res) {
|
|
|
257
257
|
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
|
|
258
258
|
<div style="text-align: center; padding: 2rem;">
|
|
259
259
|
<h1>🧠 MAMA Viewer</h1>
|
|
260
|
-
<p>Viewer
|
|
260
|
+
<p>The Viewer is only available in <strong>Standalone</strong> mode.</p>
|
|
261
261
|
<p style="color: #888; margin-top: 1rem;">Start Standalone: <code style="background: #333; padding: 0.25rem 0.5rem; border-radius: 4px;">mama start</code></p>
|
|
262
262
|
<p style="color: #666; font-size: 0.9rem; margin-top: 2rem;">Current: MCP Server (embedding only)</p>
|
|
263
263
|
</div>
|
|
@@ -313,7 +313,7 @@ async function startEmbeddingServer(port = exports.DEFAULT_PORT, options = {}) {
|
|
|
313
313
|
// Check if HTTP server is disabled
|
|
314
314
|
if (process.env.MAMA_DISABLE_HTTP_SERVER === 'true') {
|
|
315
315
|
console.error('[EmbeddingHTTP] HTTP server disabled via MAMA_DISABLE_HTTP_SERVER');
|
|
316
|
-
console.error('[EmbeddingHTTP]
|
|
316
|
+
console.error('[EmbeddingHTTP] The Viewer and the chat WebSocket API will not be available');
|
|
317
317
|
return null;
|
|
318
318
|
}
|
|
319
319
|
// Initialize database for graph API
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DatabaseAdapter as DBManagerAdapter } from '../db-manager.js';
|
|
2
|
+
export interface PrincipalRow {
|
|
3
|
+
principalId: string;
|
|
4
|
+
kind: 'owner' | 'member';
|
|
5
|
+
status: 'active' | 'suspended' | 'offboarded';
|
|
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
|
+
}
|
|
30
|
+
export interface PrincipalRepository {
|
|
31
|
+
resolveByExternal(connector: string, namespace: string, externalId: string): PrincipalRow | null;
|
|
32
|
+
registerMember(input: {
|
|
33
|
+
displayName?: string;
|
|
34
|
+
connector: string;
|
|
35
|
+
namespace: string;
|
|
36
|
+
externalId: string;
|
|
37
|
+
now: number;
|
|
38
|
+
}): string;
|
|
39
|
+
bindIdentity(principalId: string, connector: string, namespace: string, externalId: string, now: number): void;
|
|
40
|
+
suspend(principalId: string, now: number): void;
|
|
41
|
+
offboard(principalId: string, now: number): void;
|
|
42
|
+
ensureOwner(input: {
|
|
43
|
+
connector: string;
|
|
44
|
+
namespace: string;
|
|
45
|
+
externalId: string;
|
|
46
|
+
now: number;
|
|
47
|
+
}): 'created' | 'exists' | 'conflict';
|
|
48
|
+
listMembers(): Array<{
|
|
49
|
+
principalId: string;
|
|
50
|
+
displayName?: string;
|
|
51
|
+
status: string;
|
|
52
|
+
}>;
|
|
53
|
+
grantScope(input: PrincipalScopeGrantMutationInput): 'created' | 'exists';
|
|
54
|
+
revokeScope(input: PrincipalScopeGrantMutationInput): 'revoked' | 'absent';
|
|
55
|
+
listActiveGrants(principalId: string): PrincipalScopeGrantRecord[];
|
|
56
|
+
}
|
|
57
|
+
export type PrincipalRegistrationErrorCode = 'identity_bound_to_owner' | 'member_not_active';
|
|
58
|
+
export declare class PrincipalRegistrationError extends Error {
|
|
59
|
+
readonly code: PrincipalRegistrationErrorCode;
|
|
60
|
+
constructor(code: PrincipalRegistrationErrorCode, message: string);
|
|
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
|
+
}
|
|
67
|
+
export declare function createPrincipalRepository(adapter: Pick<DBManagerAdapter, 'prepare' | 'transaction'>): PrincipalRepository;
|
|
68
|
+
//# sourceMappingURL=principal-repository.d.ts.map
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrincipalScopeGrantError = exports.PrincipalRegistrationError = exports.SHARED_MEMORY_SCOPE_KINDS = void 0;
|
|
4
|
+
exports.createPrincipalRepository = createPrincipalRepository;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
exports.SHARED_MEMORY_SCOPE_KINDS = ['project', 'channel', 'global'];
|
|
7
|
+
class PrincipalRegistrationError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'PrincipalRegistrationError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
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
|
+
}
|
|
100
|
+
function mintPrincipalId(kind, connector, namespace, externalId) {
|
|
101
|
+
const identityKey = JSON.stringify([kind, connector, namespace, externalId]);
|
|
102
|
+
const digest = (0, node_crypto_1.createHash)('sha256').update(identityKey, 'utf8').digest('hex');
|
|
103
|
+
return `principal_${digest}`;
|
|
104
|
+
}
|
|
105
|
+
function mapPrincipal(row) {
|
|
106
|
+
return {
|
|
107
|
+
principalId: row.principal_id,
|
|
108
|
+
kind: row.kind,
|
|
109
|
+
status: row.status,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createPrincipalRepository(adapter) {
|
|
113
|
+
const resolveByExternalStatement = adapter.prepare(`SELECT p.principal_id, p.kind, p.status
|
|
114
|
+
FROM external_identities AS e
|
|
115
|
+
JOIN principals AS p ON p.principal_id = e.principal_id
|
|
116
|
+
WHERE e.connector = ? AND e.namespace = ? AND e.external_id = ?`);
|
|
117
|
+
const insertPrincipalStatement = adapter.prepare(`INSERT INTO principals (
|
|
118
|
+
principal_id, kind, display_name, status, created_at, updated_at
|
|
119
|
+
) VALUES (?, ?, ?, 'active', ?, ?)`);
|
|
120
|
+
const insertIdentityStatement = adapter.prepare(`INSERT INTO external_identities (
|
|
121
|
+
connector, namespace, external_id, principal_id, created_at
|
|
122
|
+
) VALUES (?, ?, ?, ?, ?)`);
|
|
123
|
+
const selectPrincipalKindStatement = adapter.prepare('SELECT kind FROM principals WHERE principal_id = ?');
|
|
124
|
+
const updatePrincipalStatusStatement = adapter.prepare('UPDATE principals SET status = ?, updated_at = ? WHERE principal_id = ?');
|
|
125
|
+
const selectActiveOwnerStatement = adapter.prepare("SELECT principal_id FROM principals WHERE kind = 'owner' AND status = 'active' LIMIT 1");
|
|
126
|
+
const listMembersStatement = adapter.prepare(`SELECT principal_id, display_name, status
|
|
127
|
+
FROM principals
|
|
128
|
+
WHERE kind = 'member'
|
|
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
|
+
}
|
|
187
|
+
function resolveByExternal(connector, namespace, externalId) {
|
|
188
|
+
const row = resolveByExternalStatement.get(connector, namespace, externalId);
|
|
189
|
+
return row ? mapPrincipal(row) : null;
|
|
190
|
+
}
|
|
191
|
+
function bindIdentity(principalId, connector, namespace, externalId, now) {
|
|
192
|
+
insertIdentityStatement.run(connector, namespace, externalId, principalId, now);
|
|
193
|
+
}
|
|
194
|
+
function registerMember(input) {
|
|
195
|
+
const existing = resolveByExternal(input.connector, input.namespace, input.externalId);
|
|
196
|
+
if (existing) {
|
|
197
|
+
if (existing.kind === 'member' && existing.status === 'active') {
|
|
198
|
+
return existing.principalId;
|
|
199
|
+
}
|
|
200
|
+
if (existing.kind === 'owner') {
|
|
201
|
+
throw new PrincipalRegistrationError('identity_bound_to_owner', 'External identity is already bound to an owner principal');
|
|
202
|
+
}
|
|
203
|
+
throw new PrincipalRegistrationError('member_not_active', 'External identity is already bound to a non-active member principal');
|
|
204
|
+
}
|
|
205
|
+
const principalId = mintPrincipalId('member', input.connector, input.namespace, input.externalId);
|
|
206
|
+
adapter.transaction(() => {
|
|
207
|
+
insertPrincipalStatement.run(principalId, 'member', input.displayName ?? null, input.now, input.now);
|
|
208
|
+
bindIdentity(principalId, input.connector, input.namespace, input.externalId, input.now);
|
|
209
|
+
});
|
|
210
|
+
return principalId;
|
|
211
|
+
}
|
|
212
|
+
function transitionMember(principalId, status, now) {
|
|
213
|
+
const principal = selectPrincipalKindStatement.get(principalId);
|
|
214
|
+
if (!principal) {
|
|
215
|
+
throw new Error(`Principal not found: ${principalId}`);
|
|
216
|
+
}
|
|
217
|
+
if (principal.kind === 'owner') {
|
|
218
|
+
throw new Error(`Owner principals cannot be ${status}`);
|
|
219
|
+
}
|
|
220
|
+
updatePrincipalStatusStatement.run(status, now, principalId);
|
|
221
|
+
}
|
|
222
|
+
function ensureOwner(input) {
|
|
223
|
+
return adapter.transaction(() => {
|
|
224
|
+
const existing = resolveByExternal(input.connector, input.namespace, input.externalId);
|
|
225
|
+
if (existing) {
|
|
226
|
+
return existing.kind === 'owner' ? 'exists' : 'conflict';
|
|
227
|
+
}
|
|
228
|
+
if (selectActiveOwnerStatement.get()) {
|
|
229
|
+
return 'conflict';
|
|
230
|
+
}
|
|
231
|
+
const principalId = mintPrincipalId('owner', input.connector, input.namespace, input.externalId);
|
|
232
|
+
insertPrincipalStatement.run(principalId, 'owner', null, input.now, input.now);
|
|
233
|
+
bindIdentity(principalId, input.connector, input.namespace, input.externalId, input.now);
|
|
234
|
+
return 'created';
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function listMembers() {
|
|
238
|
+
return listMembersStatement.all().map((row) => {
|
|
239
|
+
const member = {
|
|
240
|
+
principalId: row.principal_id,
|
|
241
|
+
status: row.status,
|
|
242
|
+
};
|
|
243
|
+
return row.display_name === null ? member : { ...member, displayName: row.display_name };
|
|
244
|
+
});
|
|
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
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
resolveByExternal,
|
|
275
|
+
registerMember,
|
|
276
|
+
bindIdentity,
|
|
277
|
+
suspend(principalId, now) {
|
|
278
|
+
transitionMember(principalId, 'suspended', now);
|
|
279
|
+
},
|
|
280
|
+
offboard(principalId, now) {
|
|
281
|
+
transitionMember(principalId, 'offboarded', now);
|
|
282
|
+
},
|
|
283
|
+
ensureOwner,
|
|
284
|
+
listMembers,
|
|
285
|
+
grantScope,
|
|
286
|
+
revokeScope,
|
|
287
|
+
listActiveGrants,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=principal-repository.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export * from './cases/wiki-page-index.js';
|
|
|
72
72
|
export * from './connectors/event-index.js';
|
|
73
73
|
export * from './connectors/raw-query.js';
|
|
74
74
|
export * from './connectors/types.js';
|
|
75
|
+
export * from './identity/principal-repository.js';
|
|
75
76
|
export * from './search/question-type.js';
|
|
76
77
|
export * from './search/feedback-store.js';
|
|
77
78
|
export * from './search/ranker-features.js';
|
package/dist/index.js
CHANGED
|
@@ -264,6 +264,7 @@ __exportStar(require("./cases/wiki-page-index.js"), exports);
|
|
|
264
264
|
__exportStar(require("./connectors/event-index.js"), exports);
|
|
265
265
|
__exportStar(require("./connectors/raw-query.js"), exports);
|
|
266
266
|
__exportStar(require("./connectors/types.js"), exports);
|
|
267
|
+
__exportStar(require("./identity/principal-repository.js"), exports);
|
|
267
268
|
__exportStar(require("./search/question-type.js"), exports);
|
|
268
269
|
__exportStar(require("./search/feedback-store.js"), exports);
|
|
269
270
|
__exportStar(require("./search/ranker-features.js"), exports);
|