@unson/brainbase-mcp 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +63 -0
- package/dist/company-os-objectives.js +234 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +104 -2
- package/dist/server.js +186 -4
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
package/dist/ontology.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { DecisionRecord, PersonalOs } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The judgment foundation is an extension manifest. Keeping this reference
|
|
4
|
+
* outside releaseV1/releaseV2 preserves the historical interpretation while
|
|
5
|
+
* giving canonical-store work one shared contract to adopt later.
|
|
6
|
+
*/
|
|
7
|
+
export { JUDGMENT_FOUNDATION_CONTRACT_VERSION, judgmentFoundationContract } from './ontology-foundation.js';
|
|
8
|
+
export declare const judgmentFoundationReference: Readonly<{
|
|
9
|
+
contractVersion: "0.1.0";
|
|
10
|
+
status: "extension-manifest";
|
|
11
|
+
historicalOntologyVersions: readonly ["1.0.0", "2.0.0"];
|
|
12
|
+
canonicalStoreAdoption: "future-story-02-plus";
|
|
13
|
+
graphActivation: "deferred";
|
|
14
|
+
}>;
|
|
2
15
|
export declare const ONTOLOGY_V1_VERSION: "1.0.0";
|
|
3
16
|
export declare const ONTOLOGY_VERSION: "2.0.0";
|
|
4
17
|
export declare const SUPPORTED_ONTOLOGY_VERSIONS: readonly ["0.0.0", "1.0.0", "2.0.0"];
|
package/dist/ontology.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { canonicalRelationRegistry } from './relation-registry.js';
|
|
2
|
+
import { JUDGMENT_FOUNDATION_CONTRACT_VERSION, judgmentFoundationContract } from './ontology-foundation.js';
|
|
3
|
+
/**
|
|
4
|
+
* The judgment foundation is an extension manifest. Keeping this reference
|
|
5
|
+
* outside releaseV1/releaseV2 preserves the historical interpretation while
|
|
6
|
+
* giving canonical-store work one shared contract to adopt later.
|
|
7
|
+
*/
|
|
8
|
+
export { JUDGMENT_FOUNDATION_CONTRACT_VERSION, judgmentFoundationContract } from './ontology-foundation.js';
|
|
9
|
+
export const judgmentFoundationReference = Object.freeze({
|
|
10
|
+
contractVersion: JUDGMENT_FOUNDATION_CONTRACT_VERSION,
|
|
11
|
+
status: judgmentFoundationContract.status,
|
|
12
|
+
historicalOntologyVersions: judgmentFoundationContract.historicalOntologyVersions,
|
|
13
|
+
canonicalStoreAdoption: judgmentFoundationContract.canonicalStoreAdoption,
|
|
14
|
+
graphActivation: judgmentFoundationContract.graphActivation
|
|
15
|
+
});
|
|
2
16
|
export const ONTOLOGY_V1_VERSION = '1.0.0';
|
|
3
17
|
export const ONTOLOGY_VERSION = '2.0.0';
|
|
4
18
|
export const SUPPORTED_ONTOLOGY_VERSIONS = ['0.0.0', ONTOLOGY_V1_VERSION, ONTOLOGY_VERSION];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
2
|
+
export type OrganizationConnectionAction = 'authorize' | 'complete' | 'read';
|
|
3
|
+
export type OrganizationConnectionStatus = 'active' | 'pending' | 'disconnected' | 'error';
|
|
4
|
+
export interface OrganizationConnectionBinding {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
personId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Provider-specific intent is kept opaque to the kernel. It is persisted only
|
|
10
|
+
* after the kernel has rejected secret-shaped keys and unsupported values.
|
|
11
|
+
*/
|
|
12
|
+
export interface OrganizationConnectionIntent extends JsonRecord {
|
|
13
|
+
}
|
|
14
|
+
export interface OrganizationConnectionStateRecord {
|
|
15
|
+
id: string;
|
|
16
|
+
stateHash: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
binding: OrganizationConnectionBinding;
|
|
19
|
+
intent: OrganizationConnectionIntent;
|
|
20
|
+
issuedAt: string;
|
|
21
|
+
expiresAt: string;
|
|
22
|
+
}
|
|
23
|
+
export type OrganizationConnectionStateConsumeResult = {
|
|
24
|
+
status: 'consumed';
|
|
25
|
+
record: OrganizationConnectionStateRecord;
|
|
26
|
+
consumedAt: string;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'missing';
|
|
29
|
+
} | {
|
|
30
|
+
status: 'expired';
|
|
31
|
+
record?: OrganizationConnectionStateRecord;
|
|
32
|
+
} | {
|
|
33
|
+
status: 'replayed';
|
|
34
|
+
record?: OrganizationConnectionStateRecord;
|
|
35
|
+
};
|
|
36
|
+
export interface OrganizationConnectionStateRepository {
|
|
37
|
+
save(record: OrganizationConnectionStateRecord): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Consume a state token atomically. Implementations must persist the
|
|
40
|
+
* consumed marker as part of the same operation that returns `consumed` so
|
|
41
|
+
* two callbacks cannot both exchange the authorization code.
|
|
42
|
+
*/
|
|
43
|
+
consume(input: {
|
|
44
|
+
stateHash: string;
|
|
45
|
+
now: string;
|
|
46
|
+
}): Promise<OrganizationConnectionStateConsumeResult>;
|
|
47
|
+
}
|
|
48
|
+
export interface OrganizationConnectionReadback {
|
|
49
|
+
provider: string;
|
|
50
|
+
connectionId: string;
|
|
51
|
+
status: OrganizationConnectionStatus;
|
|
52
|
+
displayName?: string | null;
|
|
53
|
+
externalId?: string | null;
|
|
54
|
+
scopes?: string[];
|
|
55
|
+
connectedAt?: string | null;
|
|
56
|
+
updatedAt?: string | null;
|
|
57
|
+
}
|
|
58
|
+
export interface OrganizationConnectionProviderAdapter {
|
|
59
|
+
/**
|
|
60
|
+
* Build the provider authorization URL. The raw opaque state is supplied
|
|
61
|
+
* so the adapter can put it in the provider request; the kernel never logs
|
|
62
|
+
* or audits it.
|
|
63
|
+
*/
|
|
64
|
+
createAuthorization(input: {
|
|
65
|
+
provider: string;
|
|
66
|
+
binding: OrganizationConnectionBinding;
|
|
67
|
+
state: string;
|
|
68
|
+
intent: OrganizationConnectionIntent;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
authorizationUrl: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** Exchange a code and return a safe connection projection only. */
|
|
73
|
+
exchangeAuthorizationCode(input: {
|
|
74
|
+
provider: string;
|
|
75
|
+
binding: OrganizationConnectionBinding;
|
|
76
|
+
state: OrganizationConnectionStateRecord;
|
|
77
|
+
code: string;
|
|
78
|
+
}): Promise<OrganizationConnectionReadback>;
|
|
79
|
+
/** Read a tenant/person-bound connection without returning credentials. */
|
|
80
|
+
readConnection(input: {
|
|
81
|
+
provider: string;
|
|
82
|
+
binding: OrganizationConnectionBinding;
|
|
83
|
+
connectionId?: string | null;
|
|
84
|
+
}): Promise<OrganizationConnectionReadback | null>;
|
|
85
|
+
}
|
|
86
|
+
export interface OrganizationConnectionAuthorizationRequest {
|
|
87
|
+
action: OrganizationConnectionAction;
|
|
88
|
+
provider: string;
|
|
89
|
+
binding: OrganizationConnectionBinding;
|
|
90
|
+
intent?: OrganizationConnectionIntent;
|
|
91
|
+
connectionId?: string | null;
|
|
92
|
+
}
|
|
93
|
+
export interface OrganizationConnectionPolicy {
|
|
94
|
+
authorize(request: OrganizationConnectionAuthorizationRequest): void | Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
export interface OrganizationConnectionAuditEntry {
|
|
97
|
+
action: OrganizationConnectionAction;
|
|
98
|
+
provider: string;
|
|
99
|
+
binding: OrganizationConnectionBinding;
|
|
100
|
+
stateId?: string;
|
|
101
|
+
connectionId?: string;
|
|
102
|
+
status?: OrganizationConnectionStatus;
|
|
103
|
+
occurredAt: string;
|
|
104
|
+
}
|
|
105
|
+
export interface OrganizationConnectionAudit {
|
|
106
|
+
record(entry: OrganizationConnectionAuditEntry): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
export type OrganizationConnectionClock = () => Date;
|
|
109
|
+
export interface OrganizationConnectionServiceOptions {
|
|
110
|
+
stateRepository: OrganizationConnectionStateRepository;
|
|
111
|
+
providerAdapter: OrganizationConnectionProviderAdapter;
|
|
112
|
+
policy?: OrganizationConnectionPolicy;
|
|
113
|
+
audit?: OrganizationConnectionAudit;
|
|
114
|
+
clock?: OrganizationConnectionClock;
|
|
115
|
+
stateTtlSeconds?: number;
|
|
116
|
+
}
|
|
117
|
+
export interface StartOrganizationConnectionInput {
|
|
118
|
+
provider: string;
|
|
119
|
+
binding: OrganizationConnectionBinding;
|
|
120
|
+
intent?: unknown;
|
|
121
|
+
}
|
|
122
|
+
export interface StartOrganizationConnectionResult {
|
|
123
|
+
provider: string;
|
|
124
|
+
binding: OrganizationConnectionBinding;
|
|
125
|
+
state: string;
|
|
126
|
+
expiresAt: string;
|
|
127
|
+
authorizationUrl: string;
|
|
128
|
+
}
|
|
129
|
+
export interface CompleteOrganizationConnectionInput {
|
|
130
|
+
provider: string;
|
|
131
|
+
binding: OrganizationConnectionBinding;
|
|
132
|
+
state: string;
|
|
133
|
+
code: string;
|
|
134
|
+
}
|
|
135
|
+
export interface CompleteOrganizationConnectionResult {
|
|
136
|
+
connection: OrganizationConnectionReadback;
|
|
137
|
+
consumedAt: string;
|
|
138
|
+
}
|
|
139
|
+
export interface ReadOrganizationConnectionInput {
|
|
140
|
+
provider: string;
|
|
141
|
+
binding: OrganizationConnectionBinding;
|
|
142
|
+
connectionId?: string | null;
|
|
143
|
+
}
|
|
144
|
+
export declare class OrganizationConnectionError extends Error {
|
|
145
|
+
readonly code: string;
|
|
146
|
+
readonly status: number;
|
|
147
|
+
readonly details: JsonRecord;
|
|
148
|
+
constructor(code: string, message: string, status?: number, details?: JsonRecord);
|
|
149
|
+
}
|
|
150
|
+
export declare class OrganizationConnectionService {
|
|
151
|
+
private readonly stateRepository;
|
|
152
|
+
private readonly providerAdapter;
|
|
153
|
+
private readonly policy?;
|
|
154
|
+
private readonly audit?;
|
|
155
|
+
private readonly clock;
|
|
156
|
+
private readonly stateTtlSeconds;
|
|
157
|
+
constructor(options: OrganizationConnectionServiceOptions);
|
|
158
|
+
startAuthorization(input: StartOrganizationConnectionInput): Promise<StartOrganizationConnectionResult>;
|
|
159
|
+
completeAuthorization(input: CompleteOrganizationConnectionInput): Promise<CompleteOrganizationConnectionResult>;
|
|
160
|
+
readConnection(input: ReadOrganizationConnectionInput): Promise<OrganizationConnectionReadback | null>;
|
|
161
|
+
private authorize;
|
|
162
|
+
private writeAudit;
|
|
163
|
+
}
|
|
164
|
+
export {};
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
export class OrganizationConnectionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(code, message, status = 400, details = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'OrganizationConnectionError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_STATE_TTL_SECONDS = 10 * 60;
|
|
15
|
+
const MAX_STATE_TTL_SECONDS = 60 * 60;
|
|
16
|
+
const MAX_INTENT_BYTES = 8 * 1024;
|
|
17
|
+
const MAX_CODE_LENGTH = 4 * 1024;
|
|
18
|
+
const MAX_AUTHORIZATION_URL_LENGTH = 8 * 1024;
|
|
19
|
+
const MAX_CONNECTION_ID_LENGTH = 256;
|
|
20
|
+
const SECRET_KEY_PATTERN = /(?:access|refresh|id)?[_-]?(?:token|secret)|credential|password|private[_-]?key|authorization[_-]?code|client[_-]?secret/i;
|
|
21
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
22
|
+
function fail(code, message, status = 400, details = {}) {
|
|
23
|
+
throw new OrganizationConnectionError(code, message, status, details);
|
|
24
|
+
}
|
|
25
|
+
function normalizeString(value, field, options = {}) {
|
|
26
|
+
const normalized = typeof value === 'string' ? value.normalize('NFKC').trim() : '';
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
if (options.required)
|
|
29
|
+
fail('validation_error', `${field} is required`, 400, { field });
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (CONTROL_CHARACTER_PATTERN.test(normalized)) {
|
|
33
|
+
fail('validation_error', `${field} contains control characters`, 400, { field });
|
|
34
|
+
}
|
|
35
|
+
if (options.max && normalized.length > options.max) {
|
|
36
|
+
fail('validation_error', `${field} exceeds the maximum length`, 400, {
|
|
37
|
+
field,
|
|
38
|
+
max: options.max,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
function normalizeProvider(value) {
|
|
44
|
+
const provider = normalizeString(value, 'provider', { required: true, max: 64 }).toLowerCase();
|
|
45
|
+
if (!/^[a-z][a-z0-9._-]*$/u.test(provider)) {
|
|
46
|
+
fail('validation_error', 'provider has an invalid format', 400, { field: 'provider' });
|
|
47
|
+
}
|
|
48
|
+
return provider;
|
|
49
|
+
}
|
|
50
|
+
function normalizeBinding(value) {
|
|
51
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
52
|
+
fail('validation_error', 'binding is required', 400, { field: 'binding' });
|
|
53
|
+
}
|
|
54
|
+
const binding = value;
|
|
55
|
+
return {
|
|
56
|
+
tenantId: normalizeString(binding.tenantId, 'binding.tenantId', {
|
|
57
|
+
required: true,
|
|
58
|
+
max: 256,
|
|
59
|
+
}),
|
|
60
|
+
personId: normalizeString(binding.personId, 'binding.personId', {
|
|
61
|
+
required: true,
|
|
62
|
+
max: 256,
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function normalizeState(value) {
|
|
67
|
+
return normalizeString(value, 'state', { required: true, max: 512 });
|
|
68
|
+
}
|
|
69
|
+
function normalizeCode(value) {
|
|
70
|
+
return normalizeString(value, 'code', { required: true, max: MAX_CODE_LENGTH });
|
|
71
|
+
}
|
|
72
|
+
function normalizeConnectionId(value) {
|
|
73
|
+
return normalizeString(value, 'connectionId', { max: MAX_CONNECTION_ID_LENGTH });
|
|
74
|
+
}
|
|
75
|
+
function isPlainRecord(value) {
|
|
76
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
77
|
+
return false;
|
|
78
|
+
const prototype = Object.getPrototypeOf(value);
|
|
79
|
+
return prototype === Object.prototype || prototype === null;
|
|
80
|
+
}
|
|
81
|
+
function sanitizeIntentValue(value, path, depth) {
|
|
82
|
+
if (depth > 5) {
|
|
83
|
+
fail('validation_error', 'intent is too deeply nested', 400, { field: 'intent' });
|
|
84
|
+
}
|
|
85
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
86
|
+
return value;
|
|
87
|
+
if (typeof value === 'number') {
|
|
88
|
+
if (!Number.isFinite(value)) {
|
|
89
|
+
fail('validation_error', 'intent contains a non-finite number', 400, { field: path });
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.map((entry, index) => sanitizeIntentValue(entry, `${path}[${index}]`, depth + 1));
|
|
95
|
+
}
|
|
96
|
+
if (!isPlainRecord(value)) {
|
|
97
|
+
fail('validation_error', 'intent contains an unsupported value', 400, { field: path });
|
|
98
|
+
}
|
|
99
|
+
const sanitized = {};
|
|
100
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
101
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
102
|
+
fail('validation_error', 'intent contains a sensitive field', 400, { field: path });
|
|
103
|
+
}
|
|
104
|
+
sanitized[key] = sanitizeIntentValue(entry, `${path}.${key}`, depth + 1);
|
|
105
|
+
}
|
|
106
|
+
return sanitized;
|
|
107
|
+
}
|
|
108
|
+
function normalizeIntent(value) {
|
|
109
|
+
const candidate = value == null ? {} : sanitizeIntentValue(value, 'intent', 0);
|
|
110
|
+
if (!isPlainRecord(candidate)) {
|
|
111
|
+
fail('validation_error', 'intent must be an object', 400, { field: 'intent' });
|
|
112
|
+
}
|
|
113
|
+
let serialized;
|
|
114
|
+
try {
|
|
115
|
+
serialized = JSON.stringify(candidate);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
fail('validation_error', 'intent is not serializable', 400, { field: 'intent' });
|
|
119
|
+
}
|
|
120
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_INTENT_BYTES) {
|
|
121
|
+
fail('validation_error', 'intent exceeds the maximum size', 400, {
|
|
122
|
+
field: 'intent',
|
|
123
|
+
maxBytes: MAX_INTENT_BYTES,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
function toIsoDate(value, field) {
|
|
129
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
130
|
+
fail('connection_clock_invalid', 'Connection clock returned an invalid date', 503, { field });
|
|
131
|
+
}
|
|
132
|
+
return value.toISOString();
|
|
133
|
+
}
|
|
134
|
+
function addSeconds(date, seconds) {
|
|
135
|
+
return new Date(date.getTime() + seconds * 1000);
|
|
136
|
+
}
|
|
137
|
+
function hashState(state) {
|
|
138
|
+
return createHash('sha256').update(state, 'utf8').digest('hex');
|
|
139
|
+
}
|
|
140
|
+
function sameBinding(left, right) {
|
|
141
|
+
return left.tenantId === right.tenantId && left.personId === right.personId;
|
|
142
|
+
}
|
|
143
|
+
function isSensitiveKey(key) {
|
|
144
|
+
return SECRET_KEY_PATTERN.test(key);
|
|
145
|
+
}
|
|
146
|
+
function assertNoSensitiveFields(value, depth = 0) {
|
|
147
|
+
if (depth > 5 || value === null || typeof value !== 'object')
|
|
148
|
+
return;
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
for (const entry of value)
|
|
151
|
+
assertNoSensitiveFields(entry, depth + 1);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
155
|
+
if (isSensitiveKey(key)) {
|
|
156
|
+
fail('connection_provider_response_invalid', 'Provider response contains a sensitive field', 502);
|
|
157
|
+
}
|
|
158
|
+
assertNoSensitiveFields(entry, depth + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function normalizeOptionalDate(value, field) {
|
|
162
|
+
if (value == null)
|
|
163
|
+
return value === null ? null : undefined;
|
|
164
|
+
const date = new Date(normalizeString(value, field, { required: true }));
|
|
165
|
+
if (Number.isNaN(date.getTime())) {
|
|
166
|
+
fail('connection_provider_response_invalid', 'Provider response contains an invalid date', 502, { field });
|
|
167
|
+
}
|
|
168
|
+
return date.toISOString();
|
|
169
|
+
}
|
|
170
|
+
function normalizeReadback(value, expectedProvider) {
|
|
171
|
+
if (!isPlainRecord(value)) {
|
|
172
|
+
fail('connection_provider_response_invalid', 'Provider response is invalid', 502);
|
|
173
|
+
}
|
|
174
|
+
assertNoSensitiveFields(value);
|
|
175
|
+
const provider = normalizeProvider(value.provider);
|
|
176
|
+
if (provider !== expectedProvider) {
|
|
177
|
+
fail('connection_provider_response_invalid', 'Provider response does not match the request', 502);
|
|
178
|
+
}
|
|
179
|
+
const connectionId = normalizeString(value.connectionId, 'connectionId', {
|
|
180
|
+
required: true,
|
|
181
|
+
max: MAX_CONNECTION_ID_LENGTH,
|
|
182
|
+
});
|
|
183
|
+
const status = normalizeString(value.status, 'status', { required: true, max: 32 });
|
|
184
|
+
if (!['active', 'pending', 'disconnected', 'error'].includes(status)) {
|
|
185
|
+
fail('connection_provider_response_invalid', 'Provider response contains an invalid status', 502, {
|
|
186
|
+
field: 'status',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const displayName = normalizeString(value.displayName, 'displayName', { max: 200 });
|
|
190
|
+
const externalId = normalizeString(value.externalId, 'externalId', { max: 500 });
|
|
191
|
+
let scopes;
|
|
192
|
+
if (value.scopes !== undefined) {
|
|
193
|
+
if (!Array.isArray(value.scopes)) {
|
|
194
|
+
fail('connection_provider_response_invalid', 'Provider response contains invalid scopes', 502, {
|
|
195
|
+
field: 'scopes',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
scopes = value.scopes.map((scope, index) => normalizeString(scope, `scopes[${index}]`, {
|
|
199
|
+
required: true,
|
|
200
|
+
max: 200,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
provider,
|
|
205
|
+
connectionId,
|
|
206
|
+
status: status,
|
|
207
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
208
|
+
...(externalId !== undefined ? { externalId } : {}),
|
|
209
|
+
...(scopes !== undefined ? { scopes } : {}),
|
|
210
|
+
...(value.connectedAt !== undefined
|
|
211
|
+
? { connectedAt: normalizeOptionalDate(value.connectedAt, 'connectedAt') }
|
|
212
|
+
: {}),
|
|
213
|
+
...(value.updatedAt !== undefined
|
|
214
|
+
? { updatedAt: normalizeOptionalDate(value.updatedAt, 'updatedAt') }
|
|
215
|
+
: {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function normalizeAuthorizationUrl(value) {
|
|
219
|
+
if (typeof value !== 'string') {
|
|
220
|
+
fail('connection_provider_response_invalid', 'Provider response is invalid', 502);
|
|
221
|
+
}
|
|
222
|
+
const normalized = value.normalize('NFKC').trim();
|
|
223
|
+
if (!normalized || CONTROL_CHARACTER_PATTERN.test(normalized) ||
|
|
224
|
+
normalized.length > MAX_AUTHORIZATION_URL_LENGTH) {
|
|
225
|
+
fail('connection_provider_response_invalid', 'Provider response is invalid', 502);
|
|
226
|
+
}
|
|
227
|
+
return normalized;
|
|
228
|
+
}
|
|
229
|
+
function normalizeTtl(value) {
|
|
230
|
+
const ttl = value ?? DEFAULT_STATE_TTL_SECONDS;
|
|
231
|
+
if (!Number.isInteger(ttl) || ttl < 1 || ttl > MAX_STATE_TTL_SECONDS) {
|
|
232
|
+
fail('validation_error', 'stateTtlSeconds is outside the supported range', 400, {
|
|
233
|
+
field: 'stateTtlSeconds',
|
|
234
|
+
min: 1,
|
|
235
|
+
max: MAX_STATE_TTL_SECONDS,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return ttl;
|
|
239
|
+
}
|
|
240
|
+
export class OrganizationConnectionService {
|
|
241
|
+
stateRepository;
|
|
242
|
+
providerAdapter;
|
|
243
|
+
policy;
|
|
244
|
+
audit;
|
|
245
|
+
clock;
|
|
246
|
+
stateTtlSeconds;
|
|
247
|
+
constructor(options) {
|
|
248
|
+
this.stateRepository = options.stateRepository;
|
|
249
|
+
this.providerAdapter = options.providerAdapter;
|
|
250
|
+
this.policy = options.policy;
|
|
251
|
+
this.audit = options.audit;
|
|
252
|
+
this.clock = options.clock ?? (() => new Date());
|
|
253
|
+
this.stateTtlSeconds = normalizeTtl(options.stateTtlSeconds);
|
|
254
|
+
}
|
|
255
|
+
async startAuthorization(input) {
|
|
256
|
+
const provider = normalizeProvider(input.provider);
|
|
257
|
+
const binding = normalizeBinding(input.binding);
|
|
258
|
+
const intent = normalizeIntent(input.intent);
|
|
259
|
+
await this.authorize({ action: 'authorize', provider, binding, intent });
|
|
260
|
+
const issuedAtDate = this.clock();
|
|
261
|
+
const issuedAt = toIsoDate(issuedAtDate, 'issuedAt');
|
|
262
|
+
const expiresAt = toIsoDate(addSeconds(issuedAtDate, this.stateTtlSeconds), 'expiresAt');
|
|
263
|
+
const state = randomBytes(32).toString('base64url');
|
|
264
|
+
const stateHash = hashState(state);
|
|
265
|
+
const record = {
|
|
266
|
+
id: `org-connection-state-${stateHash.slice(0, 24)}`,
|
|
267
|
+
stateHash,
|
|
268
|
+
provider,
|
|
269
|
+
binding,
|
|
270
|
+
intent,
|
|
271
|
+
issuedAt,
|
|
272
|
+
expiresAt,
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
await this.stateRepository.save(record);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
fail('connection_state_store_unavailable', 'Connection state store is unavailable', 503);
|
|
279
|
+
}
|
|
280
|
+
let authorizationResult;
|
|
281
|
+
try {
|
|
282
|
+
authorizationResult = await this.providerAdapter.createAuthorization({
|
|
283
|
+
provider,
|
|
284
|
+
binding,
|
|
285
|
+
state,
|
|
286
|
+
intent,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
fail('connection_provider_unavailable', 'Connection provider is unavailable', 503);
|
|
291
|
+
}
|
|
292
|
+
const authorizationUrl = normalizeAuthorizationUrl(authorizationResult?.authorizationUrl);
|
|
293
|
+
await this.writeAudit({
|
|
294
|
+
action: 'authorize',
|
|
295
|
+
provider,
|
|
296
|
+
binding,
|
|
297
|
+
stateId: record.id,
|
|
298
|
+
occurredAt: issuedAt,
|
|
299
|
+
});
|
|
300
|
+
return { provider, binding, state, expiresAt, authorizationUrl };
|
|
301
|
+
}
|
|
302
|
+
async completeAuthorization(input) {
|
|
303
|
+
const provider = normalizeProvider(input.provider);
|
|
304
|
+
const binding = normalizeBinding(input.binding);
|
|
305
|
+
const state = normalizeState(input.state);
|
|
306
|
+
const code = normalizeCode(input.code);
|
|
307
|
+
await this.authorize({ action: 'complete', provider, binding });
|
|
308
|
+
const nowDate = this.clock();
|
|
309
|
+
const now = toIsoDate(nowDate, 'now');
|
|
310
|
+
let consumed;
|
|
311
|
+
try {
|
|
312
|
+
consumed = await this.stateRepository.consume({ stateHash: hashState(state), now });
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
fail('connection_state_store_unavailable', 'Connection state store is unavailable', 503);
|
|
316
|
+
}
|
|
317
|
+
const consumedStatus = consumed?.status;
|
|
318
|
+
if (!['missing', 'expired', 'replayed', 'consumed'].includes(String(consumedStatus))) {
|
|
319
|
+
fail('connection_state_store_unavailable', 'Connection state store is unavailable', 503);
|
|
320
|
+
}
|
|
321
|
+
if (consumed.status === 'missing') {
|
|
322
|
+
fail('oauth_state_invalid', 'OAuth state is invalid', 400);
|
|
323
|
+
}
|
|
324
|
+
if (consumed.status === 'expired') {
|
|
325
|
+
fail('oauth_state_expired', 'OAuth state has expired', 400);
|
|
326
|
+
}
|
|
327
|
+
if (consumed.status === 'replayed') {
|
|
328
|
+
fail('oauth_state_replayed', 'OAuth state has already been used', 409);
|
|
329
|
+
}
|
|
330
|
+
const record = consumed.record;
|
|
331
|
+
if (!isPlainRecord(record) || typeof record.provider !== 'string' ||
|
|
332
|
+
!isPlainRecord(record.binding) || typeof consumed.consumedAt !== 'string') {
|
|
333
|
+
fail('connection_state_store_unavailable', 'Connection state store is unavailable', 503);
|
|
334
|
+
}
|
|
335
|
+
if (record.provider !== provider || !sameBinding(record.binding, binding)) {
|
|
336
|
+
fail('oauth_state_binding_mismatch', 'OAuth state does not match the connection request', 403);
|
|
337
|
+
}
|
|
338
|
+
let providerResult;
|
|
339
|
+
try {
|
|
340
|
+
providerResult = await this.providerAdapter.exchangeAuthorizationCode({
|
|
341
|
+
provider,
|
|
342
|
+
binding,
|
|
343
|
+
state: record,
|
|
344
|
+
code,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
fail('connection_provider_unavailable', 'Connection provider is unavailable', 503);
|
|
349
|
+
}
|
|
350
|
+
let connection;
|
|
351
|
+
try {
|
|
352
|
+
connection = normalizeReadback(providerResult, provider);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof OrganizationConnectionError && error.code === 'connection_provider_response_invalid') {
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
fail('connection_provider_response_invalid', 'Provider response is invalid', 502);
|
|
359
|
+
}
|
|
360
|
+
await this.writeAudit({
|
|
361
|
+
action: 'complete',
|
|
362
|
+
provider,
|
|
363
|
+
binding,
|
|
364
|
+
stateId: record.id,
|
|
365
|
+
connectionId: connection.connectionId,
|
|
366
|
+
status: connection.status,
|
|
367
|
+
occurredAt: consumed.consumedAt,
|
|
368
|
+
});
|
|
369
|
+
return { connection, consumedAt: consumed.consumedAt };
|
|
370
|
+
}
|
|
371
|
+
async readConnection(input) {
|
|
372
|
+
const provider = normalizeProvider(input.provider);
|
|
373
|
+
const binding = normalizeBinding(input.binding);
|
|
374
|
+
const connectionId = normalizeConnectionId(input.connectionId);
|
|
375
|
+
await this.authorize({ action: 'read', provider, binding, connectionId });
|
|
376
|
+
let result;
|
|
377
|
+
try {
|
|
378
|
+
result = await this.providerAdapter.readConnection({ provider, binding, connectionId });
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
fail('connection_provider_unavailable', 'Connection provider is unavailable', 503);
|
|
382
|
+
}
|
|
383
|
+
if (result === null)
|
|
384
|
+
return null;
|
|
385
|
+
let connection;
|
|
386
|
+
try {
|
|
387
|
+
connection = normalizeReadback(result, provider);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (error instanceof OrganizationConnectionError && error.code === 'connection_provider_response_invalid') {
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
fail('connection_provider_response_invalid', 'Provider response is invalid', 502);
|
|
394
|
+
}
|
|
395
|
+
if (connectionId && connection.connectionId !== connectionId) {
|
|
396
|
+
fail('connection_provider_response_invalid', 'Provider response does not match the request', 502);
|
|
397
|
+
}
|
|
398
|
+
await this.writeAudit({
|
|
399
|
+
action: 'read',
|
|
400
|
+
provider,
|
|
401
|
+
binding,
|
|
402
|
+
connectionId: connection.connectionId,
|
|
403
|
+
status: connection.status,
|
|
404
|
+
occurredAt: toIsoDate(this.clock(), 'occurredAt'),
|
|
405
|
+
});
|
|
406
|
+
return connection;
|
|
407
|
+
}
|
|
408
|
+
async authorize(request) {
|
|
409
|
+
if (!this.policy)
|
|
410
|
+
return;
|
|
411
|
+
try {
|
|
412
|
+
await this.policy.authorize(request);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
fail('connection_forbidden', 'Connection operation is not authorized', 403);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async writeAudit(entry) {
|
|
419
|
+
if (!this.audit)
|
|
420
|
+
return;
|
|
421
|
+
try {
|
|
422
|
+
await this.audit.record(entry);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
fail('connection_audit_unavailable', 'Connection audit is unavailable', 503);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -41,6 +41,18 @@ export interface PortableGraphReadback {
|
|
|
41
41
|
readonly bundle: unknown;
|
|
42
42
|
readonly digest: string;
|
|
43
43
|
}
|
|
44
|
+
/** One snapshot the organization stored for the token's owner. */
|
|
45
|
+
export interface PortableGraphListEntry {
|
|
46
|
+
readonly graphId: string;
|
|
47
|
+
readonly projectCode: string;
|
|
48
|
+
readonly digest: string;
|
|
49
|
+
readonly createdAt: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PortableGraphList {
|
|
52
|
+
readonly bundles: readonly PortableGraphListEntry[];
|
|
53
|
+
/** The organization returned only its newest page. */
|
|
54
|
+
readonly truncated: boolean;
|
|
55
|
+
}
|
|
44
56
|
export interface PortableGraphImportResult {
|
|
45
57
|
readonly status: 'imported' | 'unchanged';
|
|
46
58
|
readonly digest: string;
|
|
@@ -65,6 +77,7 @@ export interface OrganizationGraphClient {
|
|
|
65
77
|
search(input: GraphRetrievalInput): Promise<GraphRetrievalResponse>;
|
|
66
78
|
importPortableGraph(bundle: unknown): Promise<PortableGraphImportResult>;
|
|
67
79
|
readPortableGraph(): Promise<PortableGraphReadback>;
|
|
80
|
+
listPortableGraphs(): Promise<PortableGraphList>;
|
|
68
81
|
}
|
|
69
82
|
/** Forward the existing Graph retrieval input unchanged inside the request envelope. */
|
|
70
83
|
export declare function searchOrganizationGraph(config: OrganizationGraphConfig, input: GraphRetrievalInput): Promise<GraphRetrievalResponse>;
|
|
@@ -72,5 +85,7 @@ export declare function searchOrganizationGraph(config: OrganizationGraphConfig,
|
|
|
72
85
|
export declare function importPortableGraph(config: OrganizationGraphConfig, bundle: unknown): Promise<PortableGraphImportResult>;
|
|
73
86
|
/** Read and verify the complete organization-side bundle and its digest. */
|
|
74
87
|
export declare function readPortableGraph(config: OrganizationGraphConfig): Promise<PortableGraphReadback>;
|
|
88
|
+
/** List the token owner's snapshots. The organization decides the owner; no project or Graph ID is sent. */
|
|
89
|
+
export declare function listPortableGraphs(config: OrganizationGraphConfig): Promise<PortableGraphList>;
|
|
75
90
|
/** Validate the privacy boundary before any upload leaves the local process. */
|
|
76
91
|
export declare function assertPortableGraphBundle(bundle: unknown): asserts bundle is PortableGraphBundle;
|