@govcore/federation 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +139 -4
- package/dist/index.js +290 -14
- package/dist/index.js.map +1 -0
- package/package.json +26 -10
- package/CHANGELOG.md +0 -39
- package/dist/connections.d.ts +0 -30
- package/dist/connections.d.ts.map +0 -1
- package/dist/connections.js +0 -83
- package/dist/index.d.ts.map +0 -1
- package/dist/links.d.ts +0 -64
- package/dist/links.d.ts.map +0 -1
- package/dist/links.js +0 -214
- package/dist/visibility.d.ts +0 -45
- package/dist/visibility.d.ts.map +0 -1
- package/dist/visibility.js +0 -68
- package/src/connections.ts +0 -137
- package/src/index.ts +0 -15
- package/src/links.ts +0 -328
- package/src/visibility.ts +0 -110
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,139 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { orgConnections, GovcoreDb, crossOrgLinks } from '@govcore/schema';
|
|
2
|
+
import { SQL } from 'drizzle-orm';
|
|
3
|
+
import { AnyPgColumn } from 'drizzle-orm/pg-core';
|
|
4
|
+
|
|
5
|
+
type OrgConnection = typeof orgConnections.$inferSelect;
|
|
6
|
+
/** All connections touching `orgId`, in either direction. */
|
|
7
|
+
declare function getConnections(db: GovcoreDb, orgId: string): Promise<OrgConnection[]>;
|
|
8
|
+
/** An existing connection between two orgs in either direction, or null. */
|
|
9
|
+
declare function findConnectionBetween(db: GovcoreDb, orgId: string, targetOrgId: string): Promise<OrgConnection | null>;
|
|
10
|
+
interface RequestConnectionParams {
|
|
11
|
+
/** The org opening the connection. */
|
|
12
|
+
orgId: string;
|
|
13
|
+
/** The org being invited. */
|
|
14
|
+
targetOrgId: string;
|
|
15
|
+
/** The acting user (recorded as `created_by` and on the audit row). */
|
|
16
|
+
actorUserId: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Open a pending connection from `orgId` to `targetOrgId`. Throws if a
|
|
20
|
+
* connection already exists in either direction. Audited.
|
|
21
|
+
*/
|
|
22
|
+
declare function requestConnection(db: GovcoreDb, params: RequestConnectionParams): Promise<OrgConnection>;
|
|
23
|
+
interface ConnectionDecisionParams {
|
|
24
|
+
connectionId: string;
|
|
25
|
+
/** The org acting — must be the connection's target org. */
|
|
26
|
+
orgId: string;
|
|
27
|
+
actorUserId: string;
|
|
28
|
+
}
|
|
29
|
+
/** Accept a pending connection. Only the target org may accept. Audited. */
|
|
30
|
+
declare function acceptConnection(db: GovcoreDb, params: ConnectionDecisionParams): Promise<OrgConnection>;
|
|
31
|
+
/** Reject a pending connection. Only the target org may reject. Audited. */
|
|
32
|
+
declare function rejectConnection(db: GovcoreDb, params: ConnectionDecisionParams): Promise<OrgConnection>;
|
|
33
|
+
|
|
34
|
+
/** A content row's visibility vocabulary (mirrors the `visibility` schema enum). */
|
|
35
|
+
type FederationVisibility = 'org' | 'connections' | 'instance';
|
|
36
|
+
/**
|
|
37
|
+
* List-view scope. `org` (default) restricts to the caller's own org;
|
|
38
|
+
* `federated` is an explicit, user-requested broadening so list queries never
|
|
39
|
+
* silently fan out across every visible org.
|
|
40
|
+
*/
|
|
41
|
+
type ListScope = 'org' | 'federated';
|
|
42
|
+
/** Coerce a raw query-string value into a ListScope, defaulting to `org`. */
|
|
43
|
+
declare function parseListScope(value: string | string[] | undefined): ListScope;
|
|
44
|
+
/**
|
|
45
|
+
* Build the visibility WHERE condition for an org-scoped list query.
|
|
46
|
+
*
|
|
47
|
+
* - `org` scope → only rows owned by `orgId`.
|
|
48
|
+
* - `federated` scope → owned rows, plus instance-wide rows, plus
|
|
49
|
+
* connections/instance rows owned by a connected org.
|
|
50
|
+
*
|
|
51
|
+
* Pass `connectedOrgIds` empty under `org` scope (callers should skip the
|
|
52
|
+
* connected-org lookup there). Compose with any status filter via `and(...)`.
|
|
53
|
+
*/
|
|
54
|
+
declare function listScopeFilter(cols: {
|
|
55
|
+
organizationId: AnyPgColumn;
|
|
56
|
+
visibility: AnyPgColumn;
|
|
57
|
+
}, opts: {
|
|
58
|
+
orgId: string;
|
|
59
|
+
scope: ListScope;
|
|
60
|
+
connectedOrgIds?: string[];
|
|
61
|
+
}): SQL;
|
|
62
|
+
/**
|
|
63
|
+
* Throw if an entity's owning org doesn't match the caller's. Use before any
|
|
64
|
+
* write to content fetched without an org filter.
|
|
65
|
+
*/
|
|
66
|
+
declare function assertOwnership(entityOrgId: string | null | undefined, callerOrgId: string): void;
|
|
67
|
+
/** Org IDs with an *active* bilateral connection to `organizationId` (either direction). */
|
|
68
|
+
declare function getConnectedOrgIds(db: GovcoreDb, organizationId: string): Promise<string[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Whether `callerOrgId` may read a single entity given its owning org and
|
|
71
|
+
* visibility. Own-org always; `instance` always; `connections` only when an
|
|
72
|
+
* active connection exists. A null org/visibility is unreadable.
|
|
73
|
+
*/
|
|
74
|
+
declare function canReadFederatedEntity(db: GovcoreDb, entityOrgId: string | null | undefined, visibility: FederationVisibility | null | undefined, callerOrgId: string): Promise<boolean>;
|
|
75
|
+
|
|
76
|
+
type CrossOrgLink = typeof crossOrgLinks.$inferSelect;
|
|
77
|
+
/** What to do with a link request given any existing link for the same pair. */
|
|
78
|
+
type LinkRequestAction = 'block' | 'reactivate' | 'create';
|
|
79
|
+
/**
|
|
80
|
+
* Pure: a pending/active link blocks a new request; a previously rejected one is
|
|
81
|
+
* reactivated; otherwise a fresh link is created.
|
|
82
|
+
*/
|
|
83
|
+
declare function resolveLinkRequest(existing: Pick<CrossOrgLink, 'status'> | null | undefined): LinkRequestAction;
|
|
84
|
+
/** All links where `orgId` is either the source or target org. */
|
|
85
|
+
declare function getCrossOrgLinks(db: GovcoreDb, orgId: string): Promise<CrossOrgLink[]>;
|
|
86
|
+
/** All links touching a given entity (on either side). */
|
|
87
|
+
declare function getLinksForEntity(db: GovcoreDb, entityType: string, entityId: string): Promise<CrossOrgLink[]>;
|
|
88
|
+
interface LinkEndpoints {
|
|
89
|
+
sourceEntityType: string;
|
|
90
|
+
sourceEntityId: string;
|
|
91
|
+
targetEntityType: string;
|
|
92
|
+
targetEntityId: string;
|
|
93
|
+
}
|
|
94
|
+
/** The existing link for an exact source→target entity pair, or null. */
|
|
95
|
+
declare function findCrossOrgLink(db: GovcoreDb, ends: LinkEndpoints): Promise<CrossOrgLink | null>;
|
|
96
|
+
interface RequestCrossOrgLinkParams extends LinkEndpoints {
|
|
97
|
+
sourceOrgId: string;
|
|
98
|
+
targetOrgId: string;
|
|
99
|
+
linkType: string;
|
|
100
|
+
actorUserId: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Propose a cross-org link. Blocks if a pending/active link for the pair already
|
|
104
|
+
* exists; reactivates a previously rejected one; otherwise creates a pending
|
|
105
|
+
* link. Audited. Entity ownership/visibility must be validated by the caller.
|
|
106
|
+
*/
|
|
107
|
+
declare function requestCrossOrgLink(db: GovcoreDb, params: RequestCrossOrgLinkParams): Promise<CrossOrgLink>;
|
|
108
|
+
interface LinkDecisionParams {
|
|
109
|
+
linkId: string;
|
|
110
|
+
/** The acting org — must be the link's target org. */
|
|
111
|
+
orgId: string;
|
|
112
|
+
actorUserId: string;
|
|
113
|
+
}
|
|
114
|
+
/** Approve a pending link. Target org only. Audited. */
|
|
115
|
+
declare function approveCrossOrgLink(db: GovcoreDb, params: LinkDecisionParams): Promise<CrossOrgLink>;
|
|
116
|
+
/** Reject a pending link with an optional reason. Target org only. Audited. */
|
|
117
|
+
declare function rejectCrossOrgLink(db: GovcoreDb, params: LinkDecisionParams & {
|
|
118
|
+
reason?: string;
|
|
119
|
+
}): Promise<CrossOrgLink>;
|
|
120
|
+
/** Withdraw a link the *source* org proposed (any status). Audited. */
|
|
121
|
+
declare function withdrawCrossOrgLink(db: GovcoreDb, params: LinkDecisionParams): Promise<CrossOrgLink>;
|
|
122
|
+
/** Revoke an active link from the *target* org. Audited. */
|
|
123
|
+
declare function revokeCrossOrgLink(db: GovcoreDb, params: LinkDecisionParams): Promise<CrossOrgLink>;
|
|
124
|
+
/**
|
|
125
|
+
* Flag every pending/active link touching an entity for review — used when the
|
|
126
|
+
* entity's visibility drops so both orgs are alerted the link may no longer be
|
|
127
|
+
* accessible. Caller must have verified the entity belongs to its org.
|
|
128
|
+
*/
|
|
129
|
+
declare function flagLinksForVisibilityDrop(db: GovcoreDb, entityType: string, entityId: string, reason: string): Promise<void>;
|
|
130
|
+
/** Clear review flags on an entity's links — used when visibility is raised back. */
|
|
131
|
+
declare function clearLinksFlag(db: GovcoreDb, entityType: string, entityId: string): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Delete every cross-org link between two orgs — called when their connection is
|
|
134
|
+
* removed. Audits a snapshot of the removed links (when any) under the actor.
|
|
135
|
+
* Returns the removed rows.
|
|
136
|
+
*/
|
|
137
|
+
declare function removeLinksForConnection(db: GovcoreDb, orgAId: string, orgBId: string, actorUserId: string, actorOrgId: string): Promise<CrossOrgLink[]>;
|
|
138
|
+
|
|
139
|
+
export { type ConnectionDecisionParams, type CrossOrgLink, type FederationVisibility, type LinkDecisionParams, type LinkEndpoints, type LinkRequestAction, type ListScope, type OrgConnection, type RequestConnectionParams, type RequestCrossOrgLinkParams, acceptConnection, approveCrossOrgLink, assertOwnership, canReadFederatedEntity, clearLinksFlag, findConnectionBetween, findCrossOrgLink, flagLinksForVisibilityDrop, getConnectedOrgIds, getConnections, getCrossOrgLinks, getLinksForEntity, listScopeFilter, parseListScope, rejectConnection, rejectCrossOrgLink, removeLinksForConnection, requestConnection, requestCrossOrgLink, resolveLinkRequest, revokeCrossOrgLink, withdrawCrossOrgLink };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,290 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
// src/connections.ts
|
|
2
|
+
import { and, eq, or } from "drizzle-orm";
|
|
3
|
+
import { orgConnections } from "@govcore/schema";
|
|
4
|
+
import { writeAuditLog } from "@govcore/audit";
|
|
5
|
+
async function getConnections(db, orgId) {
|
|
6
|
+
return db.select().from(orgConnections).where(or(eq(orgConnections.fromOrgId, orgId), eq(orgConnections.toOrgId, orgId)));
|
|
7
|
+
}
|
|
8
|
+
async function findConnectionBetween(db, orgId, targetOrgId) {
|
|
9
|
+
const [existing] = await db.select().from(orgConnections).where(
|
|
10
|
+
or(
|
|
11
|
+
and(eq(orgConnections.fromOrgId, orgId), eq(orgConnections.toOrgId, targetOrgId)),
|
|
12
|
+
and(eq(orgConnections.fromOrgId, targetOrgId), eq(orgConnections.toOrgId, orgId))
|
|
13
|
+
)
|
|
14
|
+
).limit(1);
|
|
15
|
+
return existing ?? null;
|
|
16
|
+
}
|
|
17
|
+
async function requestConnection(db, params) {
|
|
18
|
+
if (params.orgId === params.targetOrgId) {
|
|
19
|
+
throw new Error("Cannot connect an organization to itself");
|
|
20
|
+
}
|
|
21
|
+
const existing = await findConnectionBetween(db, params.orgId, params.targetOrgId);
|
|
22
|
+
if (existing) throw new Error("Connection already exists or is pending");
|
|
23
|
+
const [connection] = await db.insert(orgConnections).values({
|
|
24
|
+
fromOrgId: params.orgId,
|
|
25
|
+
toOrgId: params.targetOrgId,
|
|
26
|
+
status: "pending",
|
|
27
|
+
createdBy: params.actorUserId
|
|
28
|
+
}).returning();
|
|
29
|
+
await writeAuditLog(db, {
|
|
30
|
+
action: "federation.connection.request",
|
|
31
|
+
entityType: "org_connection",
|
|
32
|
+
entityId: connection.id,
|
|
33
|
+
userId: params.actorUserId,
|
|
34
|
+
organizationId: params.orgId,
|
|
35
|
+
after: { targetOrgId: params.targetOrgId }
|
|
36
|
+
});
|
|
37
|
+
return connection;
|
|
38
|
+
}
|
|
39
|
+
async function transitionByTarget(db, params, status, action) {
|
|
40
|
+
const [row] = await db.update(orgConnections).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(
|
|
41
|
+
and(
|
|
42
|
+
eq(orgConnections.id, params.connectionId),
|
|
43
|
+
eq(orgConnections.toOrgId, params.orgId)
|
|
44
|
+
)
|
|
45
|
+
).returning();
|
|
46
|
+
if (!row) throw new Error("Connection not found or not authorized");
|
|
47
|
+
await writeAuditLog(db, {
|
|
48
|
+
action,
|
|
49
|
+
entityType: "org_connection",
|
|
50
|
+
entityId: params.connectionId,
|
|
51
|
+
userId: params.actorUserId,
|
|
52
|
+
organizationId: params.orgId
|
|
53
|
+
});
|
|
54
|
+
return row;
|
|
55
|
+
}
|
|
56
|
+
async function acceptConnection(db, params) {
|
|
57
|
+
return transitionByTarget(db, params, "active", "federation.connection.accept");
|
|
58
|
+
}
|
|
59
|
+
async function rejectConnection(db, params) {
|
|
60
|
+
return transitionByTarget(db, params, "rejected", "federation.connection.reject");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/visibility.ts
|
|
64
|
+
import { and as and2, eq as eq2, inArray, or as or2 } from "drizzle-orm";
|
|
65
|
+
import { orgConnections as orgConnections2 } from "@govcore/schema";
|
|
66
|
+
function parseListScope(value) {
|
|
67
|
+
return value === "federated" ? "federated" : "org";
|
|
68
|
+
}
|
|
69
|
+
function listScopeFilter(cols, opts) {
|
|
70
|
+
const base = eq2(cols.organizationId, opts.orgId);
|
|
71
|
+
if (opts.scope === "org") return base;
|
|
72
|
+
const instanceWide = eq2(cols.visibility, "instance");
|
|
73
|
+
const connectedOrgIds = opts.connectedOrgIds ?? [];
|
|
74
|
+
if (connectedOrgIds.length === 0) return or2(base, instanceWide);
|
|
75
|
+
return or2(
|
|
76
|
+
base,
|
|
77
|
+
instanceWide,
|
|
78
|
+
and2(
|
|
79
|
+
inArray(cols.organizationId, connectedOrgIds),
|
|
80
|
+
inArray(cols.visibility, ["connections", "instance"])
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
function assertOwnership(entityOrgId, callerOrgId) {
|
|
85
|
+
if (!entityOrgId || entityOrgId !== callerOrgId) {
|
|
86
|
+
throw new Error("Forbidden: content owned by another organization");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function getConnectedOrgIds(db, organizationId) {
|
|
90
|
+
const rows = await db.select({ fromOrgId: orgConnections2.fromOrgId, toOrgId: orgConnections2.toOrgId }).from(orgConnections2).where(
|
|
91
|
+
and2(
|
|
92
|
+
or2(
|
|
93
|
+
eq2(orgConnections2.fromOrgId, organizationId),
|
|
94
|
+
eq2(orgConnections2.toOrgId, organizationId)
|
|
95
|
+
),
|
|
96
|
+
eq2(orgConnections2.status, "active")
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
return rows.map((r) => r.fromOrgId === organizationId ? r.toOrgId : r.fromOrgId);
|
|
100
|
+
}
|
|
101
|
+
async function canReadFederatedEntity(db, entityOrgId, visibility, callerOrgId) {
|
|
102
|
+
if (!entityOrgId || !visibility) return false;
|
|
103
|
+
if (entityOrgId === callerOrgId) return true;
|
|
104
|
+
if (visibility === "instance") return true;
|
|
105
|
+
if (visibility !== "connections") return false;
|
|
106
|
+
const connectedOrgIds = await getConnectedOrgIds(db, callerOrgId);
|
|
107
|
+
return connectedOrgIds.includes(entityOrgId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/links.ts
|
|
111
|
+
import { and as and3, eq as eq3, inArray as inArray2, or as or3 } from "drizzle-orm";
|
|
112
|
+
import { crossOrgLinks } from "@govcore/schema";
|
|
113
|
+
import { writeAuditLog as writeAuditLog2 } from "@govcore/audit";
|
|
114
|
+
function resolveLinkRequest(existing) {
|
|
115
|
+
if (!existing) return "create";
|
|
116
|
+
if (existing.status === "pending" || existing.status === "active") return "block";
|
|
117
|
+
return "reactivate";
|
|
118
|
+
}
|
|
119
|
+
var touchesEntity = (entityType, entityId) => or3(
|
|
120
|
+
and3(eq3(crossOrgLinks.sourceEntityType, entityType), eq3(crossOrgLinks.sourceEntityId, entityId)),
|
|
121
|
+
and3(eq3(crossOrgLinks.targetEntityType, entityType), eq3(crossOrgLinks.targetEntityId, entityId))
|
|
122
|
+
);
|
|
123
|
+
var betweenOrgs = (orgAId, orgBId) => or3(
|
|
124
|
+
and3(eq3(crossOrgLinks.sourceOrgId, orgAId), eq3(crossOrgLinks.targetOrgId, orgBId)),
|
|
125
|
+
and3(eq3(crossOrgLinks.sourceOrgId, orgBId), eq3(crossOrgLinks.targetOrgId, orgAId))
|
|
126
|
+
);
|
|
127
|
+
async function getCrossOrgLinks(db, orgId) {
|
|
128
|
+
return db.select().from(crossOrgLinks).where(or3(eq3(crossOrgLinks.sourceOrgId, orgId), eq3(crossOrgLinks.targetOrgId, orgId)));
|
|
129
|
+
}
|
|
130
|
+
async function getLinksForEntity(db, entityType, entityId) {
|
|
131
|
+
return db.select().from(crossOrgLinks).where(touchesEntity(entityType, entityId));
|
|
132
|
+
}
|
|
133
|
+
async function findCrossOrgLink(db, ends) {
|
|
134
|
+
const [row] = await db.select().from(crossOrgLinks).where(
|
|
135
|
+
and3(
|
|
136
|
+
eq3(crossOrgLinks.sourceEntityType, ends.sourceEntityType),
|
|
137
|
+
eq3(crossOrgLinks.sourceEntityId, ends.sourceEntityId),
|
|
138
|
+
eq3(crossOrgLinks.targetEntityType, ends.targetEntityType),
|
|
139
|
+
eq3(crossOrgLinks.targetEntityId, ends.targetEntityId)
|
|
140
|
+
)
|
|
141
|
+
).limit(1);
|
|
142
|
+
return row ?? null;
|
|
143
|
+
}
|
|
144
|
+
async function requestCrossOrgLink(db, params) {
|
|
145
|
+
if (params.sourceOrgId === params.targetOrgId) {
|
|
146
|
+
throw new Error("Use same-org relationships for links within one organization");
|
|
147
|
+
}
|
|
148
|
+
const existing = await findCrossOrgLink(db, params);
|
|
149
|
+
const action = resolveLinkRequest(existing);
|
|
150
|
+
if (action === "block") {
|
|
151
|
+
throw new Error("A cross-org link already exists or is awaiting approval");
|
|
152
|
+
}
|
|
153
|
+
let link;
|
|
154
|
+
if (action === "reactivate" && existing) {
|
|
155
|
+
const [row] = await db.update(crossOrgLinks).set({ linkType: params.linkType, status: "pending", rejectionReason: null, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(crossOrgLinks.id, existing.id)).returning();
|
|
156
|
+
link = row;
|
|
157
|
+
} else {
|
|
158
|
+
const [row] = await db.insert(crossOrgLinks).values({
|
|
159
|
+
sourceOrgId: params.sourceOrgId,
|
|
160
|
+
sourceEntityType: params.sourceEntityType,
|
|
161
|
+
sourceEntityId: params.sourceEntityId,
|
|
162
|
+
targetOrgId: params.targetOrgId,
|
|
163
|
+
targetEntityType: params.targetEntityType,
|
|
164
|
+
targetEntityId: params.targetEntityId,
|
|
165
|
+
linkType: params.linkType,
|
|
166
|
+
status: "pending",
|
|
167
|
+
createdBy: params.actorUserId
|
|
168
|
+
}).returning();
|
|
169
|
+
link = row;
|
|
170
|
+
}
|
|
171
|
+
await writeAuditLog2(db, {
|
|
172
|
+
action: "federation.cross_org_link.request",
|
|
173
|
+
entityType: "cross_org_link",
|
|
174
|
+
entityId: link.id,
|
|
175
|
+
userId: params.actorUserId,
|
|
176
|
+
organizationId: params.sourceOrgId,
|
|
177
|
+
after: {
|
|
178
|
+
sourceEntityId: params.sourceEntityId,
|
|
179
|
+
targetEntityId: params.targetEntityId,
|
|
180
|
+
linkType: params.linkType,
|
|
181
|
+
targetOrgId: params.targetOrgId
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return link;
|
|
185
|
+
}
|
|
186
|
+
async function transitionByTarget2(db, params, status, action) {
|
|
187
|
+
const [link] = await db.select().from(crossOrgLinks).where(and3(eq3(crossOrgLinks.id, params.linkId), eq3(crossOrgLinks.targetOrgId, params.orgId))).limit(1);
|
|
188
|
+
if (!link) throw new Error("Cross-org link not found or not authorized");
|
|
189
|
+
if (link.status !== "pending") throw new Error("Only pending links can be approved or rejected");
|
|
190
|
+
const [row] = await db.update(crossOrgLinks).set({
|
|
191
|
+
status,
|
|
192
|
+
rejectionReason: status === "rejected" && params.reason?.trim() ? params.reason.trim() : null,
|
|
193
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
194
|
+
}).where(eq3(crossOrgLinks.id, params.linkId)).returning();
|
|
195
|
+
await writeAuditLog2(db, {
|
|
196
|
+
action,
|
|
197
|
+
entityType: "cross_org_link",
|
|
198
|
+
entityId: params.linkId,
|
|
199
|
+
userId: params.actorUserId,
|
|
200
|
+
organizationId: params.orgId,
|
|
201
|
+
after: { sourceEntityId: link.sourceEntityId, targetEntityId: link.targetEntityId }
|
|
202
|
+
});
|
|
203
|
+
return row;
|
|
204
|
+
}
|
|
205
|
+
async function approveCrossOrgLink(db, params) {
|
|
206
|
+
return transitionByTarget2(db, params, "active", "federation.cross_org_link.approve");
|
|
207
|
+
}
|
|
208
|
+
async function rejectCrossOrgLink(db, params) {
|
|
209
|
+
return transitionByTarget2(db, params, "rejected", "federation.cross_org_link.reject");
|
|
210
|
+
}
|
|
211
|
+
async function deleteOwnedLink(db, params, ownerColumn, action) {
|
|
212
|
+
const [link] = await db.select().from(crossOrgLinks).where(and3(eq3(crossOrgLinks.id, params.linkId), eq3(ownerColumn, params.orgId))).limit(1);
|
|
213
|
+
if (!link) throw new Error("Cross-org link not found or not authorized");
|
|
214
|
+
if (params.requireActive && link.status !== "active") {
|
|
215
|
+
throw new Error("Only active links can be revoked");
|
|
216
|
+
}
|
|
217
|
+
await db.delete(crossOrgLinks).where(eq3(crossOrgLinks.id, params.linkId));
|
|
218
|
+
await writeAuditLog2(db, {
|
|
219
|
+
action,
|
|
220
|
+
entityType: "cross_org_link",
|
|
221
|
+
entityId: params.linkId,
|
|
222
|
+
userId: params.actorUserId,
|
|
223
|
+
organizationId: params.orgId,
|
|
224
|
+
before: {
|
|
225
|
+
sourceEntityId: link.sourceEntityId,
|
|
226
|
+
targetEntityId: link.targetEntityId,
|
|
227
|
+
status: link.status
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return link;
|
|
231
|
+
}
|
|
232
|
+
async function withdrawCrossOrgLink(db, params) {
|
|
233
|
+
return deleteOwnedLink(db, params, crossOrgLinks.sourceOrgId, "federation.cross_org_link.withdraw");
|
|
234
|
+
}
|
|
235
|
+
async function revokeCrossOrgLink(db, params) {
|
|
236
|
+
return deleteOwnedLink(
|
|
237
|
+
db,
|
|
238
|
+
{ ...params, requireActive: true },
|
|
239
|
+
crossOrgLinks.targetOrgId,
|
|
240
|
+
"federation.cross_org_link.revoke"
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
async function flagLinksForVisibilityDrop(db, entityType, entityId, reason) {
|
|
244
|
+
await db.update(crossOrgLinks).set({ flaggedForReview: true, flagReason: reason, updatedAt: /* @__PURE__ */ new Date() }).where(and3(touchesEntity(entityType, entityId), inArray2(crossOrgLinks.status, ["pending", "active"])));
|
|
245
|
+
}
|
|
246
|
+
async function clearLinksFlag(db, entityType, entityId) {
|
|
247
|
+
await db.update(crossOrgLinks).set({ flaggedForReview: false, flagReason: null, updatedAt: /* @__PURE__ */ new Date() }).where(and3(touchesEntity(entityType, entityId), eq3(crossOrgLinks.flaggedForReview, true)));
|
|
248
|
+
}
|
|
249
|
+
async function removeLinksForConnection(db, orgAId, orgBId, actorUserId, actorOrgId) {
|
|
250
|
+
const affected = await db.select().from(crossOrgLinks).where(betweenOrgs(orgAId, orgBId));
|
|
251
|
+
if (affected.length === 0) return [];
|
|
252
|
+
await db.delete(crossOrgLinks).where(betweenOrgs(orgAId, orgBId));
|
|
253
|
+
await writeAuditLog2(db, {
|
|
254
|
+
action: "federation.cross_org_link.remove_for_connection",
|
|
255
|
+
entityType: "org_connection",
|
|
256
|
+
userId: actorUserId,
|
|
257
|
+
organizationId: actorOrgId,
|
|
258
|
+
before: {
|
|
259
|
+
orgAId,
|
|
260
|
+
orgBId,
|
|
261
|
+
removedLinkIds: affected.map((l) => l.id)
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return affected;
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
acceptConnection,
|
|
268
|
+
approveCrossOrgLink,
|
|
269
|
+
assertOwnership,
|
|
270
|
+
canReadFederatedEntity,
|
|
271
|
+
clearLinksFlag,
|
|
272
|
+
findConnectionBetween,
|
|
273
|
+
findCrossOrgLink,
|
|
274
|
+
flagLinksForVisibilityDrop,
|
|
275
|
+
getConnectedOrgIds,
|
|
276
|
+
getConnections,
|
|
277
|
+
getCrossOrgLinks,
|
|
278
|
+
getLinksForEntity,
|
|
279
|
+
listScopeFilter,
|
|
280
|
+
parseListScope,
|
|
281
|
+
rejectConnection,
|
|
282
|
+
rejectCrossOrgLink,
|
|
283
|
+
removeLinksForConnection,
|
|
284
|
+
requestConnection,
|
|
285
|
+
requestCrossOrgLink,
|
|
286
|
+
resolveLinkRequest,
|
|
287
|
+
revokeCrossOrgLink,
|
|
288
|
+
withdrawCrossOrgLink
|
|
289
|
+
};
|
|
290
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/connections.ts","../src/visibility.ts","../src/links.ts"],"sourcesContent":["// @govcore/federation/connections — the org-to-org connection lifecycle.\n//\n// A connection is a bilateral, explicit link between two orgs. The requester's\n// org opens it (`pending`); only the *target* org may accept (`active`) or\n// reject (`rejected`). An active connection is what `getConnectedOrgIds` and the\n// federated read checks key off. Each transition is audited. App-layer auth\n// (who is an admin, whose org is acting) stays in the caller.\n\nimport { and, eq, or } from 'drizzle-orm'\nimport { orgConnections, type GovcoreDb } from '@govcore/schema'\nimport { writeAuditLog } from '@govcore/audit'\n\nexport type OrgConnection = typeof orgConnections.$inferSelect\n\n/** All connections touching `orgId`, in either direction. */\nexport async function getConnections(db: GovcoreDb, orgId: string): Promise<OrgConnection[]> {\n return db\n .select()\n .from(orgConnections)\n .where(or(eq(orgConnections.fromOrgId, orgId), eq(orgConnections.toOrgId, orgId)))\n}\n\n/** An existing connection between two orgs in either direction, or null. */\nexport async function findConnectionBetween(\n db: GovcoreDb,\n orgId: string,\n targetOrgId: string,\n): Promise<OrgConnection | null> {\n const [existing] = await db\n .select()\n .from(orgConnections)\n .where(\n or(\n and(eq(orgConnections.fromOrgId, orgId), eq(orgConnections.toOrgId, targetOrgId)),\n and(eq(orgConnections.fromOrgId, targetOrgId), eq(orgConnections.toOrgId, orgId)),\n ),\n )\n .limit(1)\n return existing ?? null\n}\n\nexport interface RequestConnectionParams {\n /** The org opening the connection. */\n orgId: string\n /** The org being invited. */\n targetOrgId: string\n /** The acting user (recorded as `created_by` and on the audit row). */\n actorUserId: string\n}\n\n/**\n * Open a pending connection from `orgId` to `targetOrgId`. Throws if a\n * connection already exists in either direction. Audited.\n */\nexport async function requestConnection(\n db: GovcoreDb,\n params: RequestConnectionParams,\n): Promise<OrgConnection> {\n if (params.orgId === params.targetOrgId) {\n throw new Error('Cannot connect an organization to itself')\n }\n const existing = await findConnectionBetween(db, params.orgId, params.targetOrgId)\n if (existing) throw new Error('Connection already exists or is pending')\n\n const [connection] = await db\n .insert(orgConnections)\n .values({\n fromOrgId: params.orgId,\n toOrgId: params.targetOrgId,\n status: 'pending',\n createdBy: params.actorUserId,\n })\n .returning()\n\n await writeAuditLog(db, {\n action: 'federation.connection.request',\n entityType: 'org_connection',\n entityId: connection.id,\n userId: params.actorUserId,\n organizationId: params.orgId,\n after: { targetOrgId: params.targetOrgId },\n })\n return connection\n}\n\n/** Internal: a target-only status transition with an audit row. */\nasync function transitionByTarget(\n db: GovcoreDb,\n params: { connectionId: string; orgId: string; actorUserId: string },\n status: 'active' | 'rejected',\n action: string,\n): Promise<OrgConnection> {\n // Only the target org (toOrgId) may accept or reject.\n const [row] = await db\n .update(orgConnections)\n .set({ status, updatedAt: new Date() })\n .where(\n and(\n eq(orgConnections.id, params.connectionId),\n eq(orgConnections.toOrgId, params.orgId),\n ),\n )\n .returning()\n if (!row) throw new Error('Connection not found or not authorized')\n\n await writeAuditLog(db, {\n action,\n entityType: 'org_connection',\n entityId: params.connectionId,\n userId: params.actorUserId,\n organizationId: params.orgId,\n })\n return row\n}\n\nexport interface ConnectionDecisionParams {\n connectionId: string\n /** The org acting — must be the connection's target org. */\n orgId: string\n actorUserId: string\n}\n\n/** Accept a pending connection. Only the target org may accept. Audited. */\nexport async function acceptConnection(\n db: GovcoreDb,\n params: ConnectionDecisionParams,\n): Promise<OrgConnection> {\n return transitionByTarget(db, params, 'active', 'federation.connection.accept')\n}\n\n/** Reject a pending connection. Only the target org may reject. Audited. */\nexport async function rejectConnection(\n db: GovcoreDb,\n params: ConnectionDecisionParams,\n): Promise<OrgConnection> {\n return transitionByTarget(db, params, 'rejected', 'federation.connection.reject')\n}\n","// @govcore/federation/visibility — federated read scoping.\n//\n// Content rows carry a `visibility` (org | connections | instance). A list query\n// is scoped to the caller's org by default; broadening to `federated` also pulls\n// in instance-wide rows and rows owned by a *connected* org whose visibility\n// permits it. These helpers build the query condition and answer single-row\n// read checks; the content tables themselves are app-owned (content is a later\n// milestone), so callers pass the columns.\n\nimport { and, eq, inArray, or, type SQL } from 'drizzle-orm'\nimport type { AnyPgColumn } from 'drizzle-orm/pg-core'\nimport { orgConnections, type GovcoreDb } from '@govcore/schema'\n\n/** A content row's visibility vocabulary (mirrors the `visibility` schema enum). */\nexport type FederationVisibility = 'org' | 'connections' | 'instance'\n\n/**\n * List-view scope. `org` (default) restricts to the caller's own org;\n * `federated` is an explicit, user-requested broadening so list queries never\n * silently fan out across every visible org.\n */\nexport type ListScope = 'org' | 'federated'\n\n/** Coerce a raw query-string value into a ListScope, defaulting to `org`. */\nexport function parseListScope(value: string | string[] | undefined): ListScope {\n return value === 'federated' ? 'federated' : 'org'\n}\n\n/**\n * Build the visibility WHERE condition for an org-scoped list query.\n *\n * - `org` scope → only rows owned by `orgId`.\n * - `federated` scope → owned rows, plus instance-wide rows, plus\n * connections/instance rows owned by a connected org.\n *\n * Pass `connectedOrgIds` empty under `org` scope (callers should skip the\n * connected-org lookup there). Compose with any status filter via `and(...)`.\n */\nexport function listScopeFilter(\n cols: { organizationId: AnyPgColumn; visibility: AnyPgColumn },\n opts: { orgId: string; scope: ListScope; connectedOrgIds?: string[] },\n): SQL {\n const base = eq(cols.organizationId, opts.orgId)\n if (opts.scope === 'org') return base\n\n const instanceWide = eq(cols.visibility, 'instance')\n const connectedOrgIds = opts.connectedOrgIds ?? []\n if (connectedOrgIds.length === 0) return or(base, instanceWide)!\n return or(\n base,\n instanceWide,\n and(\n inArray(cols.organizationId, connectedOrgIds),\n inArray(cols.visibility, ['connections', 'instance']),\n ),\n )!\n}\n\n/**\n * Throw if an entity's owning org doesn't match the caller's. Use before any\n * write to content fetched without an org filter.\n */\nexport function assertOwnership(\n entityOrgId: string | null | undefined,\n callerOrgId: string,\n): void {\n if (!entityOrgId || entityOrgId !== callerOrgId) {\n throw new Error('Forbidden: content owned by another organization')\n }\n}\n\n/** Org IDs with an *active* bilateral connection to `organizationId` (either direction). */\nexport async function getConnectedOrgIds(\n db: GovcoreDb,\n organizationId: string,\n): Promise<string[]> {\n const rows = await db\n .select({ fromOrgId: orgConnections.fromOrgId, toOrgId: orgConnections.toOrgId })\n .from(orgConnections)\n .where(\n and(\n or(\n eq(orgConnections.fromOrgId, organizationId),\n eq(orgConnections.toOrgId, organizationId),\n ),\n eq(orgConnections.status, 'active'),\n ),\n )\n return rows.map((r) => (r.fromOrgId === organizationId ? r.toOrgId : r.fromOrgId))\n}\n\n/**\n * Whether `callerOrgId` may read a single entity given its owning org and\n * visibility. Own-org always; `instance` always; `connections` only when an\n * active connection exists. A null org/visibility is unreadable.\n */\nexport async function canReadFederatedEntity(\n db: GovcoreDb,\n entityOrgId: string | null | undefined,\n visibility: FederationVisibility | null | undefined,\n callerOrgId: string,\n): Promise<boolean> {\n if (!entityOrgId || !visibility) return false\n if (entityOrgId === callerOrgId) return true\n if (visibility === 'instance') return true\n if (visibility !== 'connections') return false\n\n const connectedOrgIds = await getConnectedOrgIds(db, callerOrgId)\n return connectedOrgIds.includes(entityOrgId)\n}\n","// @govcore/federation/links — the cross-org content-link lifecycle.\n//\n// A cross-org link is an approved relationship between a content item in one org\n// and one in another. Entity ids carry no FK (they cross org boundaries) and the\n// link semantics are app-defined: `linkType`, `sourceEntityType`/`targetEntityType`\n// are plain strings here. The *source* org proposes a link; only the *target* org\n// approves or rejects it. Entity ownership and federated-visibility preconditions\n// are the caller's responsibility (they live over app-owned content) — this module\n// owns the link record and its transitions. Each transition is audited.\n\nimport { and, eq, inArray, or } from 'drizzle-orm'\nimport { crossOrgLinks, type GovcoreDb } from '@govcore/schema'\nimport { writeAuditLog } from '@govcore/audit'\n\nexport type CrossOrgLink = typeof crossOrgLinks.$inferSelect\n\n/** What to do with a link request given any existing link for the same pair. */\nexport type LinkRequestAction = 'block' | 'reactivate' | 'create'\n\n/**\n * Pure: a pending/active link blocks a new request; a previously rejected one is\n * reactivated; otherwise a fresh link is created.\n */\nexport function resolveLinkRequest(\n existing: Pick<CrossOrgLink, 'status'> | null | undefined,\n): LinkRequestAction {\n if (!existing) return 'create'\n if (existing.status === 'pending' || existing.status === 'active') return 'block'\n return 'reactivate'\n}\n\nconst touchesEntity = (entityType: string, entityId: string) =>\n or(\n and(eq(crossOrgLinks.sourceEntityType, entityType), eq(crossOrgLinks.sourceEntityId, entityId)),\n and(eq(crossOrgLinks.targetEntityType, entityType), eq(crossOrgLinks.targetEntityId, entityId)),\n )\n\nconst betweenOrgs = (orgAId: string, orgBId: string) =>\n or(\n and(eq(crossOrgLinks.sourceOrgId, orgAId), eq(crossOrgLinks.targetOrgId, orgBId)),\n and(eq(crossOrgLinks.sourceOrgId, orgBId), eq(crossOrgLinks.targetOrgId, orgAId)),\n )\n\n/** All links where `orgId` is either the source or target org. */\nexport async function getCrossOrgLinks(db: GovcoreDb, orgId: string): Promise<CrossOrgLink[]> {\n return db\n .select()\n .from(crossOrgLinks)\n .where(or(eq(crossOrgLinks.sourceOrgId, orgId), eq(crossOrgLinks.targetOrgId, orgId)))\n}\n\n/** All links touching a given entity (on either side). */\nexport async function getLinksForEntity(\n db: GovcoreDb,\n entityType: string,\n entityId: string,\n): Promise<CrossOrgLink[]> {\n return db.select().from(crossOrgLinks).where(touchesEntity(entityType, entityId))\n}\n\nexport interface LinkEndpoints {\n sourceEntityType: string\n sourceEntityId: string\n targetEntityType: string\n targetEntityId: string\n}\n\n/** The existing link for an exact source→target entity pair, or null. */\nexport async function findCrossOrgLink(\n db: GovcoreDb,\n ends: LinkEndpoints,\n): Promise<CrossOrgLink | null> {\n const [row] = await db\n .select()\n .from(crossOrgLinks)\n .where(\n and(\n eq(crossOrgLinks.sourceEntityType, ends.sourceEntityType),\n eq(crossOrgLinks.sourceEntityId, ends.sourceEntityId),\n eq(crossOrgLinks.targetEntityType, ends.targetEntityType),\n eq(crossOrgLinks.targetEntityId, ends.targetEntityId),\n ),\n )\n .limit(1)\n return row ?? null\n}\n\nexport interface RequestCrossOrgLinkParams extends LinkEndpoints {\n sourceOrgId: string\n targetOrgId: string\n linkType: string\n actorUserId: string\n}\n\n/**\n * Propose a cross-org link. Blocks if a pending/active link for the pair already\n * exists; reactivates a previously rejected one; otherwise creates a pending\n * link. Audited. Entity ownership/visibility must be validated by the caller.\n */\nexport async function requestCrossOrgLink(\n db: GovcoreDb,\n params: RequestCrossOrgLinkParams,\n): Promise<CrossOrgLink> {\n if (params.sourceOrgId === params.targetOrgId) {\n throw new Error('Use same-org relationships for links within one organization')\n }\n\n const existing = await findCrossOrgLink(db, params)\n const action = resolveLinkRequest(existing)\n if (action === 'block') {\n throw new Error('A cross-org link already exists or is awaiting approval')\n }\n\n let link: CrossOrgLink\n if (action === 'reactivate' && existing) {\n const [row] = await db\n .update(crossOrgLinks)\n .set({ linkType: params.linkType, status: 'pending', rejectionReason: null, updatedAt: new Date() })\n .where(eq(crossOrgLinks.id, existing.id))\n .returning()\n link = row\n } else {\n const [row] = await db\n .insert(crossOrgLinks)\n .values({\n sourceOrgId: params.sourceOrgId,\n sourceEntityType: params.sourceEntityType,\n sourceEntityId: params.sourceEntityId,\n targetOrgId: params.targetOrgId,\n targetEntityType: params.targetEntityType,\n targetEntityId: params.targetEntityId,\n linkType: params.linkType,\n status: 'pending',\n createdBy: params.actorUserId,\n })\n .returning()\n link = row\n }\n\n await writeAuditLog(db, {\n action: 'federation.cross_org_link.request',\n entityType: 'cross_org_link',\n entityId: link.id,\n userId: params.actorUserId,\n organizationId: params.sourceOrgId,\n after: {\n sourceEntityId: params.sourceEntityId,\n targetEntityId: params.targetEntityId,\n linkType: params.linkType,\n targetOrgId: params.targetOrgId,\n },\n })\n return link\n}\n\n/** Internal: a target-org-only status transition with an audit row. */\nasync function transitionByTarget(\n db: GovcoreDb,\n params: { linkId: string; orgId: string; actorUserId: string; reason?: string },\n status: 'active' | 'rejected',\n action: string,\n): Promise<CrossOrgLink> {\n const [link] = await db\n .select()\n .from(crossOrgLinks)\n .where(and(eq(crossOrgLinks.id, params.linkId), eq(crossOrgLinks.targetOrgId, params.orgId)))\n .limit(1)\n if (!link) throw new Error('Cross-org link not found or not authorized')\n if (link.status !== 'pending') throw new Error('Only pending links can be approved or rejected')\n\n const [row] = await db\n .update(crossOrgLinks)\n .set({\n status,\n rejectionReason: status === 'rejected' && params.reason?.trim() ? params.reason.trim() : null,\n updatedAt: new Date(),\n })\n .where(eq(crossOrgLinks.id, params.linkId))\n .returning()\n\n await writeAuditLog(db, {\n action,\n entityType: 'cross_org_link',\n entityId: params.linkId,\n userId: params.actorUserId,\n organizationId: params.orgId,\n after: { sourceEntityId: link.sourceEntityId, targetEntityId: link.targetEntityId },\n })\n return row\n}\n\nexport interface LinkDecisionParams {\n linkId: string\n /** The acting org — must be the link's target org. */\n orgId: string\n actorUserId: string\n}\n\n/** Approve a pending link. Target org only. Audited. */\nexport async function approveCrossOrgLink(\n db: GovcoreDb,\n params: LinkDecisionParams,\n): Promise<CrossOrgLink> {\n return transitionByTarget(db, params, 'active', 'federation.cross_org_link.approve')\n}\n\n/** Reject a pending link with an optional reason. Target org only. Audited. */\nexport async function rejectCrossOrgLink(\n db: GovcoreDb,\n params: LinkDecisionParams & { reason?: string },\n): Promise<CrossOrgLink> {\n return transitionByTarget(db, params, 'rejected', 'federation.cross_org_link.reject')\n}\n\n/** Internal: delete a link the acting org owns on the given side, audited. */\nasync function deleteOwnedLink(\n db: GovcoreDb,\n params: { linkId: string; orgId: string; actorUserId: string; requireActive?: boolean },\n ownerColumn: typeof crossOrgLinks.sourceOrgId | typeof crossOrgLinks.targetOrgId,\n action: string,\n): Promise<CrossOrgLink> {\n const [link] = await db\n .select()\n .from(crossOrgLinks)\n .where(and(eq(crossOrgLinks.id, params.linkId), eq(ownerColumn, params.orgId)))\n .limit(1)\n if (!link) throw new Error('Cross-org link not found or not authorized')\n if (params.requireActive && link.status !== 'active') {\n throw new Error('Only active links can be revoked')\n }\n\n await db.delete(crossOrgLinks).where(eq(crossOrgLinks.id, params.linkId))\n\n await writeAuditLog(db, {\n action,\n entityType: 'cross_org_link',\n entityId: params.linkId,\n userId: params.actorUserId,\n organizationId: params.orgId,\n before: {\n sourceEntityId: link.sourceEntityId,\n targetEntityId: link.targetEntityId,\n status: link.status,\n },\n })\n return link\n}\n\n/** Withdraw a link the *source* org proposed (any status). Audited. */\nexport async function withdrawCrossOrgLink(\n db: GovcoreDb,\n params: LinkDecisionParams,\n): Promise<CrossOrgLink> {\n return deleteOwnedLink(db, params, crossOrgLinks.sourceOrgId, 'federation.cross_org_link.withdraw')\n}\n\n/** Revoke an active link from the *target* org. Audited. */\nexport async function revokeCrossOrgLink(\n db: GovcoreDb,\n params: LinkDecisionParams,\n): Promise<CrossOrgLink> {\n return deleteOwnedLink(\n db,\n { ...params, requireActive: true },\n crossOrgLinks.targetOrgId,\n 'federation.cross_org_link.revoke',\n )\n}\n\n/**\n * Flag every pending/active link touching an entity for review — used when the\n * entity's visibility drops so both orgs are alerted the link may no longer be\n * accessible. Caller must have verified the entity belongs to its org.\n */\nexport async function flagLinksForVisibilityDrop(\n db: GovcoreDb,\n entityType: string,\n entityId: string,\n reason: string,\n): Promise<void> {\n await db\n .update(crossOrgLinks)\n .set({ flaggedForReview: true, flagReason: reason, updatedAt: new Date() })\n .where(and(touchesEntity(entityType, entityId), inArray(crossOrgLinks.status, ['pending', 'active'])))\n}\n\n/** Clear review flags on an entity's links — used when visibility is raised back. */\nexport async function clearLinksFlag(\n db: GovcoreDb,\n entityType: string,\n entityId: string,\n): Promise<void> {\n await db\n .update(crossOrgLinks)\n .set({ flaggedForReview: false, flagReason: null, updatedAt: new Date() })\n .where(and(touchesEntity(entityType, entityId), eq(crossOrgLinks.flaggedForReview, true)))\n}\n\n/**\n * Delete every cross-org link between two orgs — called when their connection is\n * removed. Audits a snapshot of the removed links (when any) under the actor.\n * Returns the removed rows.\n */\nexport async function removeLinksForConnection(\n db: GovcoreDb,\n orgAId: string,\n orgBId: string,\n actorUserId: string,\n actorOrgId: string,\n): Promise<CrossOrgLink[]> {\n const affected = await db.select().from(crossOrgLinks).where(betweenOrgs(orgAId, orgBId))\n if (affected.length === 0) return []\n\n await db.delete(crossOrgLinks).where(betweenOrgs(orgAId, orgBId))\n\n await writeAuditLog(db, {\n action: 'federation.cross_org_link.remove_for_connection',\n entityType: 'org_connection',\n userId: actorUserId,\n organizationId: actorOrgId,\n before: {\n orgAId,\n orgBId,\n removedLinkIds: affected.map((l) => l.id),\n },\n })\n return affected\n}\n"],"mappings":";AAQA,SAAS,KAAK,IAAI,UAAU;AAC5B,SAAS,sBAAsC;AAC/C,SAAS,qBAAqB;AAK9B,eAAsB,eAAe,IAAe,OAAyC;AAC3F,SAAO,GACJ,OAAO,EACP,KAAK,cAAc,EACnB,MAAM,GAAG,GAAG,eAAe,WAAW,KAAK,GAAG,GAAG,eAAe,SAAS,KAAK,CAAC,CAAC;AACrF;AAGA,eAAsB,sBACpB,IACA,OACA,aAC+B;AAC/B,QAAM,CAAC,QAAQ,IAAI,MAAM,GACtB,OAAO,EACP,KAAK,cAAc,EACnB;AAAA,IACC;AAAA,MACE,IAAI,GAAG,eAAe,WAAW,KAAK,GAAG,GAAG,eAAe,SAAS,WAAW,CAAC;AAAA,MAChF,IAAI,GAAG,eAAe,WAAW,WAAW,GAAG,GAAG,eAAe,SAAS,KAAK,CAAC;AAAA,IAClF;AAAA,EACF,EACC,MAAM,CAAC;AACV,SAAO,YAAY;AACrB;AAeA,eAAsB,kBACpB,IACA,QACwB;AACxB,MAAI,OAAO,UAAU,OAAO,aAAa;AACvC,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,QAAM,WAAW,MAAM,sBAAsB,IAAI,OAAO,OAAO,OAAO,WAAW;AACjF,MAAI,SAAU,OAAM,IAAI,MAAM,yCAAyC;AAEvE,QAAM,CAAC,UAAU,IAAI,MAAM,GACxB,OAAO,cAAc,EACrB,OAAO;AAAA,IACN,WAAW,OAAO;AAAA,IAClB,SAAS,OAAO;AAAA,IAChB,QAAQ;AAAA,IACR,WAAW,OAAO;AAAA,EACpB,CAAC,EACA,UAAU;AAEb,QAAM,cAAc,IAAI;AAAA,IACtB,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,UAAU,WAAW;AAAA,IACrB,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA,IACvB,OAAO,EAAE,aAAa,OAAO,YAAY;AAAA,EAC3C,CAAC;AACD,SAAO;AACT;AAGA,eAAe,mBACb,IACA,QACA,QACA,QACwB;AAExB,QAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO,cAAc,EACrB,IAAI,EAAE,QAAQ,WAAW,oBAAI,KAAK,EAAE,CAAC,EACrC;AAAA,IACC;AAAA,MACE,GAAG,eAAe,IAAI,OAAO,YAAY;AAAA,MACzC,GAAG,eAAe,SAAS,OAAO,KAAK;AAAA,IACzC;AAAA,EACF,EACC,UAAU;AACb,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,wCAAwC;AAElE,QAAM,cAAc,IAAI;AAAA,IACtB;AAAA,IACA,YAAY;AAAA,IACZ,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA,EACzB,CAAC;AACD,SAAO;AACT;AAUA,eAAsB,iBACpB,IACA,QACwB;AACxB,SAAO,mBAAmB,IAAI,QAAQ,UAAU,8BAA8B;AAChF;AAGA,eAAsB,iBACpB,IACA,QACwB;AACxB,SAAO,mBAAmB,IAAI,QAAQ,YAAY,8BAA8B;AAClF;;;AC/HA,SAAS,OAAAA,MAAK,MAAAC,KAAI,SAAS,MAAAC,WAAoB;AAE/C,SAAS,kBAAAC,uBAAsC;AAaxC,SAAS,eAAe,OAAiD;AAC9E,SAAO,UAAU,cAAc,cAAc;AAC/C;AAYO,SAAS,gBACd,MACA,MACK;AACL,QAAM,OAAOF,IAAG,KAAK,gBAAgB,KAAK,KAAK;AAC/C,MAAI,KAAK,UAAU,MAAO,QAAO;AAEjC,QAAM,eAAeA,IAAG,KAAK,YAAY,UAAU;AACnD,QAAM,kBAAkB,KAAK,mBAAmB,CAAC;AACjD,MAAI,gBAAgB,WAAW,EAAG,QAAOC,IAAG,MAAM,YAAY;AAC9D,SAAOA;AAAA,IACL;AAAA,IACA;AAAA,IACAF;AAAA,MACE,QAAQ,KAAK,gBAAgB,eAAe;AAAA,MAC5C,QAAQ,KAAK,YAAY,CAAC,eAAe,UAAU,CAAC;AAAA,IACtD;AAAA,EACF;AACF;AAMO,SAAS,gBACd,aACA,aACM;AACN,MAAI,CAAC,eAAe,gBAAgB,aAAa;AAC/C,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACF;AAGA,eAAsB,mBACpB,IACA,gBACmB;AACnB,QAAM,OAAO,MAAM,GAChB,OAAO,EAAE,WAAWG,gBAAe,WAAW,SAASA,gBAAe,QAAQ,CAAC,EAC/E,KAAKA,eAAc,EACnB;AAAA,IACCH;AAAA,MACEE;AAAA,QACED,IAAGE,gBAAe,WAAW,cAAc;AAAA,QAC3CF,IAAGE,gBAAe,SAAS,cAAc;AAAA,MAC3C;AAAA,MACAF,IAAGE,gBAAe,QAAQ,QAAQ;AAAA,IACpC;AAAA,EACF;AACF,SAAO,KAAK,IAAI,CAAC,MAAO,EAAE,cAAc,iBAAiB,EAAE,UAAU,EAAE,SAAU;AACnF;AAOA,eAAsB,uBACpB,IACA,aACA,YACA,aACkB;AAClB,MAAI,CAAC,eAAe,CAAC,WAAY,QAAO;AACxC,MAAI,gBAAgB,YAAa,QAAO;AACxC,MAAI,eAAe,WAAY,QAAO;AACtC,MAAI,eAAe,cAAe,QAAO;AAEzC,QAAM,kBAAkB,MAAM,mBAAmB,IAAI,WAAW;AAChE,SAAO,gBAAgB,SAAS,WAAW;AAC7C;;;ACnGA,SAAS,OAAAC,MAAK,MAAAC,KAAI,WAAAC,UAAS,MAAAC,WAAU;AACrC,SAAS,qBAAqC;AAC9C,SAAS,iBAAAC,sBAAqB;AAWvB,SAAS,mBACd,UACmB;AACnB,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,SAAS,WAAW,aAAa,SAAS,WAAW,SAAU,QAAO;AAC1E,SAAO;AACT;AAEA,IAAM,gBAAgB,CAAC,YAAoB,aACzCD;AAAA,EACEH,KAAIC,IAAG,cAAc,kBAAkB,UAAU,GAAGA,IAAG,cAAc,gBAAgB,QAAQ,CAAC;AAAA,EAC9FD,KAAIC,IAAG,cAAc,kBAAkB,UAAU,GAAGA,IAAG,cAAc,gBAAgB,QAAQ,CAAC;AAChG;AAEF,IAAM,cAAc,CAAC,QAAgB,WACnCE;AAAA,EACEH,KAAIC,IAAG,cAAc,aAAa,MAAM,GAAGA,IAAG,cAAc,aAAa,MAAM,CAAC;AAAA,EAChFD,KAAIC,IAAG,cAAc,aAAa,MAAM,GAAGA,IAAG,cAAc,aAAa,MAAM,CAAC;AAClF;AAGF,eAAsB,iBAAiB,IAAe,OAAwC;AAC5F,SAAO,GACJ,OAAO,EACP,KAAK,aAAa,EAClB,MAAME,IAAGF,IAAG,cAAc,aAAa,KAAK,GAAGA,IAAG,cAAc,aAAa,KAAK,CAAC,CAAC;AACzF;AAGA,eAAsB,kBACpB,IACA,YACA,UACyB;AACzB,SAAO,GAAG,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,YAAY,QAAQ,CAAC;AAClF;AAUA,eAAsB,iBACpB,IACA,MAC8B;AAC9B,QAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO,EACP,KAAK,aAAa,EAClB;AAAA,IACCD;AAAA,MACEC,IAAG,cAAc,kBAAkB,KAAK,gBAAgB;AAAA,MACxDA,IAAG,cAAc,gBAAgB,KAAK,cAAc;AAAA,MACpDA,IAAG,cAAc,kBAAkB,KAAK,gBAAgB;AAAA,MACxDA,IAAG,cAAc,gBAAgB,KAAK,cAAc;AAAA,IACtD;AAAA,EACF,EACC,MAAM,CAAC;AACV,SAAO,OAAO;AAChB;AAcA,eAAsB,oBACpB,IACA,QACuB;AACvB,MAAI,OAAO,gBAAgB,OAAO,aAAa;AAC7C,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAChF;AAEA,QAAM,WAAW,MAAM,iBAAiB,IAAI,MAAM;AAClD,QAAM,SAAS,mBAAmB,QAAQ;AAC1C,MAAI,WAAW,SAAS;AACtB,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,MAAI;AACJ,MAAI,WAAW,gBAAgB,UAAU;AACvC,UAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO,aAAa,EACpB,IAAI,EAAE,UAAU,OAAO,UAAU,QAAQ,WAAW,iBAAiB,MAAM,WAAW,oBAAI,KAAK,EAAE,CAAC,EAClG,MAAMA,IAAG,cAAc,IAAI,SAAS,EAAE,CAAC,EACvC,UAAU;AACb,WAAO;AAAA,EACT,OAAO;AACL,UAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO,aAAa,EACpB,OAAO;AAAA,MACN,aAAa,OAAO;AAAA,MACpB,kBAAkB,OAAO;AAAA,MACzB,gBAAgB,OAAO;AAAA,MACvB,aAAa,OAAO;AAAA,MACpB,kBAAkB,OAAO;AAAA,MACzB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,QAAQ;AAAA,MACR,WAAW,OAAO;AAAA,IACpB,CAAC,EACA,UAAU;AACb,WAAO;AAAA,EACT;AAEA,QAAMG,eAAc,IAAI;AAAA,IACtB,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,UAAU,KAAK;AAAA,IACf,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA,IACvB,OAAO;AAAA,MACL,gBAAgB,OAAO;AAAA,MACvB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,aAAa,OAAO;AAAA,IACtB;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAGA,eAAeC,oBACb,IACA,QACA,QACA,QACuB;AACvB,QAAM,CAAC,IAAI,IAAI,MAAM,GAClB,OAAO,EACP,KAAK,aAAa,EAClB,MAAML,KAAIC,IAAG,cAAc,IAAI,OAAO,MAAM,GAAGA,IAAG,cAAc,aAAa,OAAO,KAAK,CAAC,CAAC,EAC3F,MAAM,CAAC;AACV,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4CAA4C;AACvE,MAAI,KAAK,WAAW,UAAW,OAAM,IAAI,MAAM,gDAAgD;AAE/F,QAAM,CAAC,GAAG,IAAI,MAAM,GACjB,OAAO,aAAa,EACpB,IAAI;AAAA,IACH;AAAA,IACA,iBAAiB,WAAW,cAAc,OAAO,QAAQ,KAAK,IAAI,OAAO,OAAO,KAAK,IAAI;AAAA,IACzF,WAAW,oBAAI,KAAK;AAAA,EACtB,CAAC,EACA,MAAMA,IAAG,cAAc,IAAI,OAAO,MAAM,CAAC,EACzC,UAAU;AAEb,QAAMG,eAAc,IAAI;AAAA,IACtB;AAAA,IACA,YAAY;AAAA,IACZ,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA,IACvB,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,gBAAgB,KAAK,eAAe;AAAA,EACpF,CAAC;AACD,SAAO;AACT;AAUA,eAAsB,oBACpB,IACA,QACuB;AACvB,SAAOC,oBAAmB,IAAI,QAAQ,UAAU,mCAAmC;AACrF;AAGA,eAAsB,mBACpB,IACA,QACuB;AACvB,SAAOA,oBAAmB,IAAI,QAAQ,YAAY,kCAAkC;AACtF;AAGA,eAAe,gBACb,IACA,QACA,aACA,QACuB;AACvB,QAAM,CAAC,IAAI,IAAI,MAAM,GAClB,OAAO,EACP,KAAK,aAAa,EAClB,MAAML,KAAIC,IAAG,cAAc,IAAI,OAAO,MAAM,GAAGA,IAAG,aAAa,OAAO,KAAK,CAAC,CAAC,EAC7E,MAAM,CAAC;AACV,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4CAA4C;AACvE,MAAI,OAAO,iBAAiB,KAAK,WAAW,UAAU;AACpD,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAEA,QAAM,GAAG,OAAO,aAAa,EAAE,MAAMA,IAAG,cAAc,IAAI,OAAO,MAAM,CAAC;AAExE,QAAMG,eAAc,IAAI;AAAA,IACtB;AAAA,IACA,YAAY;AAAA,IACZ,UAAU,OAAO;AAAA,IACjB,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA,IACvB,QAAQ;AAAA,MACN,gBAAgB,KAAK;AAAA,MACrB,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,IACf;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAGA,eAAsB,qBACpB,IACA,QACuB;AACvB,SAAO,gBAAgB,IAAI,QAAQ,cAAc,aAAa,oCAAoC;AACpG;AAGA,eAAsB,mBACpB,IACA,QACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA,EAAE,GAAG,QAAQ,eAAe,KAAK;AAAA,IACjC,cAAc;AAAA,IACd;AAAA,EACF;AACF;AAOA,eAAsB,2BACpB,IACA,YACA,UACA,QACe;AACf,QAAM,GACH,OAAO,aAAa,EACpB,IAAI,EAAE,kBAAkB,MAAM,YAAY,QAAQ,WAAW,oBAAI,KAAK,EAAE,CAAC,EACzE,MAAMJ,KAAI,cAAc,YAAY,QAAQ,GAAGE,SAAQ,cAAc,QAAQ,CAAC,WAAW,QAAQ,CAAC,CAAC,CAAC;AACzG;AAGA,eAAsB,eACpB,IACA,YACA,UACe;AACf,QAAM,GACH,OAAO,aAAa,EACpB,IAAI,EAAE,kBAAkB,OAAO,YAAY,MAAM,WAAW,oBAAI,KAAK,EAAE,CAAC,EACxE,MAAMF,KAAI,cAAc,YAAY,QAAQ,GAAGC,IAAG,cAAc,kBAAkB,IAAI,CAAC,CAAC;AAC7F;AAOA,eAAsB,yBACpB,IACA,QACA,QACA,aACA,YACyB;AACzB,QAAM,WAAW,MAAM,GAAG,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,QAAQ,MAAM,CAAC;AACxF,MAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAEnC,QAAM,GAAG,OAAO,aAAa,EAAE,MAAM,YAAY,QAAQ,MAAM,CAAC;AAEhE,QAAMG,eAAc,IAAI;AAAA,IACtB,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,QAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA,gBAAgB,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,IAC1C;AAAA,EACF,CAAC;AACD,SAAO;AACT;","names":["and","eq","or","orgConnections","and","eq","inArray","or","writeAuditLog","transitionByTarget"]}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govcore/federation",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Cross-organization connections and federated-visibility query helpers",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
@@ -16,14 +16,30 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"drizzle-orm": "^0.45.2",
|
|
19
|
-
"@govcore/audit": "0.1.
|
|
20
|
-
"@govcore/schema": "0.1
|
|
19
|
+
"@govcore/audit": "0.1.2",
|
|
20
|
+
"@govcore/schema": "0.2.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"typescript": "^5.5.0"
|
|
23
|
+
"typescript": "^5.5.0",
|
|
24
|
+
"tsup": "^8.3.0"
|
|
25
|
+
},
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"tsup": {
|
|
31
|
+
"entry": [
|
|
32
|
+
"src/index.ts"
|
|
33
|
+
],
|
|
34
|
+
"format": [
|
|
35
|
+
"esm"
|
|
36
|
+
],
|
|
37
|
+
"dts": true,
|
|
38
|
+
"clean": true,
|
|
39
|
+
"sourcemap": true
|
|
24
40
|
},
|
|
25
41
|
"scripts": {
|
|
26
|
-
"build": "
|
|
27
|
-
"dev": "
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch"
|
|
28
44
|
}
|
|
29
45
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# @govcore/federation
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- 1d1794f: Add the cross-org content-link lifecycle to `@govcore/federation`. Entity types
|
|
8
|
-
and `linkType` are app-defined strings; the app validates entity
|
|
9
|
-
ownership/visibility, this owns the link record:
|
|
10
|
-
|
|
11
|
-
- reads — `getCrossOrgLinks`, `getLinksForEntity`, `findCrossOrgLink`;
|
|
12
|
-
- lifecycle (audited) — `requestCrossOrgLink` (blocks an existing pending/active
|
|
13
|
-
pair, reactivates a rejected one), `approveCrossOrgLink` / `rejectCrossOrgLink`
|
|
14
|
-
(target org only), `withdrawCrossOrgLink` (source org), `revokeCrossOrgLink`
|
|
15
|
-
(target org, active only);
|
|
16
|
-
- review flags + connection cleanup — `flagLinksForVisibilityDrop`,
|
|
17
|
-
`clearLinksFlag`, `removeLinksForConnection`;
|
|
18
|
-
- pure helper `resolveLinkRequest`.
|
|
19
|
-
|
|
20
|
-
## 0.1.0
|
|
21
|
-
|
|
22
|
-
### Minor Changes
|
|
23
|
-
|
|
24
|
-
- Phase 4 — `@govcore/federation`: cross-organization connections and federated
|
|
25
|
-
visibility.
|
|
26
|
-
|
|
27
|
-
connections: the bilateral request / accept / reject lifecycle (audited; only
|
|
28
|
-
the target org may accept or reject), with `getConnections` /
|
|
29
|
-
`findConnectionBetween`. visibility: `FederationVisibility` / `ListScope`,
|
|
30
|
-
`parseListScope`, the generic `listScopeFilter`, `assertOwnership`,
|
|
31
|
-
`getConnectedOrgIds`, and `canReadFederatedEntity`. Cross-org content links are
|
|
32
|
-
not part of this release.
|
|
33
|
-
|
|
34
|
-
### Patch Changes
|
|
35
|
-
|
|
36
|
-
- Updated dependencies
|
|
37
|
-
- Updated dependencies [f2f3743]
|
|
38
|
-
- @govcore/audit@0.1.0
|
|
39
|
-
- @govcore/schema@0.1.0
|
package/dist/connections.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { orgConnections, type GovcoreDb } from '@govcore/schema';
|
|
2
|
-
export type OrgConnection = typeof orgConnections.$inferSelect;
|
|
3
|
-
/** All connections touching `orgId`, in either direction. */
|
|
4
|
-
export declare function getConnections(db: GovcoreDb, orgId: string): Promise<OrgConnection[]>;
|
|
5
|
-
/** An existing connection between two orgs in either direction, or null. */
|
|
6
|
-
export declare function findConnectionBetween(db: GovcoreDb, orgId: string, targetOrgId: string): Promise<OrgConnection | null>;
|
|
7
|
-
export interface RequestConnectionParams {
|
|
8
|
-
/** The org opening the connection. */
|
|
9
|
-
orgId: string;
|
|
10
|
-
/** The org being invited. */
|
|
11
|
-
targetOrgId: string;
|
|
12
|
-
/** The acting user (recorded as `created_by` and on the audit row). */
|
|
13
|
-
actorUserId: string;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Open a pending connection from `orgId` to `targetOrgId`. Throws if a
|
|
17
|
-
* connection already exists in either direction. Audited.
|
|
18
|
-
*/
|
|
19
|
-
export declare function requestConnection(db: GovcoreDb, params: RequestConnectionParams): Promise<OrgConnection>;
|
|
20
|
-
export interface ConnectionDecisionParams {
|
|
21
|
-
connectionId: string;
|
|
22
|
-
/** The org acting — must be the connection's target org. */
|
|
23
|
-
orgId: string;
|
|
24
|
-
actorUserId: string;
|
|
25
|
-
}
|
|
26
|
-
/** Accept a pending connection. Only the target org may accept. Audited. */
|
|
27
|
-
export declare function acceptConnection(db: GovcoreDb, params: ConnectionDecisionParams): Promise<OrgConnection>;
|
|
28
|
-
/** Reject a pending connection. Only the target org may reject. Audited. */
|
|
29
|
-
export declare function rejectConnection(db: GovcoreDb, params: ConnectionDecisionParams): Promise<OrgConnection>;
|
|
30
|
-
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../src/connections.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhE,MAAM,MAAM,aAAa,GAAG,OAAO,cAAc,CAAC,YAAY,CAAA;AAE9D,6DAA6D;AAC7D,wBAAsB,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAK3F;AAED,4EAA4E;AAC5E,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAY/B;AAED,MAAM,WAAW,uBAAuB;IACtC,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,aAAa,CAAC,CA0BxB;AAgCD,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,4EAA4E;AAC5E,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,4EAA4E;AAC5E,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAExB"}
|