@inixiative/signet 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/contracts.ts +24 -1
- package/src/events.ts +169 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/signet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Present Kingdom Signets: the wire contracts, DPoP proofs, device pairing and a renewing client shared by Kingdom, Archive, Foundry and Oracle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
37
37
|
"@biomejs/biome": "2.5.2",
|
|
38
|
-
"@inixiative/config": "^0.5.
|
|
38
|
+
"@inixiative/config": "^0.5.171",
|
|
39
39
|
"@types/bun": "1.4.4",
|
|
40
40
|
"lefthook": "2.1.9",
|
|
41
41
|
"typescript": "6.0.3",
|
package/src/contracts.ts
CHANGED
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
archiveOperationSchema,
|
|
9
9
|
narrowsArchiveLens,
|
|
10
10
|
} from './archive';
|
|
11
|
+
import {
|
|
12
|
+
eventsInputSchemas,
|
|
13
|
+
eventsLensSchema,
|
|
14
|
+
eventsOperationSchema,
|
|
15
|
+
eventsResourceKind,
|
|
16
|
+
narrowsEventsLens,
|
|
17
|
+
} from './events';
|
|
11
18
|
import {
|
|
12
19
|
githubInputSchemas,
|
|
13
20
|
githubOperationSchema,
|
|
@@ -29,6 +36,7 @@ import {
|
|
|
29
36
|
} from './threads';
|
|
30
37
|
|
|
31
38
|
export * from './archive';
|
|
39
|
+
export * from './events';
|
|
32
40
|
export * from './github';
|
|
33
41
|
export * from './threads';
|
|
34
42
|
|
|
@@ -93,6 +101,7 @@ export const signetLensSchema = z.union([
|
|
|
93
101
|
archiveLensSchema,
|
|
94
102
|
threadsLensSchema,
|
|
95
103
|
repositoryLensSchema,
|
|
104
|
+
eventsLensSchema,
|
|
96
105
|
]);
|
|
97
106
|
export type SignetLens = z.infer<typeof signetLensSchema>;
|
|
98
107
|
|
|
@@ -104,7 +113,9 @@ export const resourceLensSchema = (kind: string) =>
|
|
|
104
113
|
? threadsLensSchema
|
|
105
114
|
: kind === repositoryResourceKind
|
|
106
115
|
? repositoryLensSchema
|
|
107
|
-
:
|
|
116
|
+
: kind === eventsResourceKind
|
|
117
|
+
? eventsLensSchema
|
|
118
|
+
: emptyLensSchema;
|
|
108
119
|
|
|
109
120
|
/**
|
|
110
121
|
* `granted` reaches nothing `requested` doesn't, for lenses of the same resource. Approving someone
|
|
@@ -114,6 +125,16 @@ export const resourceLensSchema = (kind: string) =>
|
|
|
114
125
|
export const narrowsSignetLens = (granted: SignetLens, requested: SignetLens): boolean => {
|
|
115
126
|
if ('threads' in granted || 'threads' in requested)
|
|
116
127
|
return 'threads' in granted && 'threads' in requested && narrowsThreadsLens(granted, requested);
|
|
128
|
+
const isEvents = (lens: SignetLens) => 'where' in lens || 'select' in lens || 'name' in lens;
|
|
129
|
+
if (isEvents(granted) || isEvents(requested)) {
|
|
130
|
+
const eventsGranted = eventsLensSchema.safeParse(granted);
|
|
131
|
+
const eventsRequested = eventsLensSchema.safeParse(requested);
|
|
132
|
+
return (
|
|
133
|
+
eventsGranted.success &&
|
|
134
|
+
eventsRequested.success &&
|
|
135
|
+
narrowsEventsLens(eventsGranted.data, eventsRequested.data)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
117
138
|
if ('commits' in granted || 'commits' in requested) {
|
|
118
139
|
const repositoryGranted = repositoryLensSchema.safeParse(granted);
|
|
119
140
|
const repositoryRequested = repositoryLensSchema.safeParse(requested);
|
|
@@ -477,12 +498,14 @@ export const declaredOperationSchema = z.enum([
|
|
|
477
498
|
...threadOperationSchema.options,
|
|
478
499
|
...archiveOperationSchema.options,
|
|
479
500
|
...githubOperationSchema.options,
|
|
501
|
+
...eventsOperationSchema.options,
|
|
480
502
|
]);
|
|
481
503
|
export type DeclaredOperation = z.infer<typeof declaredOperationSchema>;
|
|
482
504
|
export const operationInputSchemas = {
|
|
483
505
|
...threadInputSchemas,
|
|
484
506
|
...archiveInputSchemas,
|
|
485
507
|
...githubInputSchemas,
|
|
508
|
+
...eventsInputSchemas,
|
|
486
509
|
} as const satisfies Record<DeclaredOperation, z.ZodObject>;
|
|
487
510
|
export type OperationInput<O extends DeclaredOperation> = z.infer<
|
|
488
511
|
(typeof operationInputSchemas)[O]
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration events (Kingdom `docs/INTEGRATION-EVENTS.md`, Aron 2026-11-01). An integration that
|
|
5
|
+
* receives its provider's events (a webhook endpoint, or a poll where the provider has none) has one
|
|
6
|
+
* `events` resource: its feed. A Signet grant of `events.subscribe` on it is a **channel**: a
|
|
7
|
+
* json-rules `where` over the event and an optional `select` of what the holder receives. The holder
|
|
8
|
+
* reads its Signet to see the channels it has, and subscribes to any or all of them; Kingdom matches
|
|
9
|
+
* each arriving event against the integration's channels the same way. Live only: a channel nobody
|
|
10
|
+
* holds open drops the event.
|
|
11
|
+
*/
|
|
12
|
+
export const eventsResourceKind = 'events';
|
|
13
|
+
|
|
14
|
+
/** A provider identity an event names: a login or handle, and the provider's id when it gives one. */
|
|
15
|
+
export const eventPartySchema = z.strictObject({
|
|
16
|
+
login: z.string().min(1).max(200),
|
|
17
|
+
id: z.string().min(1).max(200).optional(),
|
|
18
|
+
});
|
|
19
|
+
export type EventParty = z.infer<typeof eventPartySchema>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One provider event, normalized. `eventId` is the provider's delivery id (or the notification id
|
|
23
|
+
* for a poll), so a retried delivery is one event. `type` is `<provider event>.<action>`, e.g.
|
|
24
|
+
* `issue_comment.created`. `payload` holds only the fields Kingdom allows for that type; the rest of
|
|
25
|
+
* a provider body never leaves Kingdom.
|
|
26
|
+
*/
|
|
27
|
+
export const integrationEventSchema = z.strictObject({
|
|
28
|
+
eventId: z.string().min(1).max(200),
|
|
29
|
+
integrationId: z.uuid(),
|
|
30
|
+
provider: z.string().min(1).max(80),
|
|
31
|
+
type: z.string().min(1).max(120),
|
|
32
|
+
at: z.iso.datetime(),
|
|
33
|
+
actor: eventPartySchema.nullable(),
|
|
34
|
+
/** Where it happened: `owner/name` for a repository, a channel or team id elsewhere. */
|
|
35
|
+
scope: z.string().min(1).max(300).nullable(),
|
|
36
|
+
/** Who it names: mentioned, assigned, asked to review. */
|
|
37
|
+
mentions: z.array(z.string().min(1).max(200)).max(200),
|
|
38
|
+
assignees: z.array(z.string().min(1).max(200)).max(200),
|
|
39
|
+
requestedReviewers: z.array(z.string().min(1).max(200)).max(200),
|
|
40
|
+
/** What it is about, for a holder that shows it: a title, a link, a number. */
|
|
41
|
+
subject: z.strictObject({
|
|
42
|
+
title: z.string().max(1000).optional(),
|
|
43
|
+
url: z.url().max(2000).optional(),
|
|
44
|
+
number: z.number().int().optional(),
|
|
45
|
+
}),
|
|
46
|
+
/** A short excerpt of the text (a comment, a message), when the type has one. */
|
|
47
|
+
excerpt: z.string().max(2000).optional(),
|
|
48
|
+
payload: z.record(z.string(), z.unknown()),
|
|
49
|
+
});
|
|
50
|
+
export type IntegrationEvent = z.infer<typeof integrationEventSchema>;
|
|
51
|
+
|
|
52
|
+
/** The envelope fields a channel may `select`. */
|
|
53
|
+
export const integrationEventFieldSchema = z.enum([
|
|
54
|
+
'eventId',
|
|
55
|
+
'integrationId',
|
|
56
|
+
'provider',
|
|
57
|
+
'type',
|
|
58
|
+
'at',
|
|
59
|
+
'actor',
|
|
60
|
+
'scope',
|
|
61
|
+
'mentions',
|
|
62
|
+
'assignees',
|
|
63
|
+
'requestedReviewers',
|
|
64
|
+
'subject',
|
|
65
|
+
'excerpt',
|
|
66
|
+
'payload',
|
|
67
|
+
]);
|
|
68
|
+
export type IntegrationEventField = z.infer<typeof integrationEventFieldSchema>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A channel: `where` is a json-rules condition over the event (Kingdom validates it with
|
|
72
|
+
* json-rules against `integrationEventSchema`'s fields; this package only carries it as JSON), a
|
|
73
|
+
* positive condition, so a grant without one matches every event. `select` is the overlay
|
|
74
|
+
* restricting what the holder receives; `eventId` and `type` are always sent. The `where` may test
|
|
75
|
+
* fields the `select` leaves out. `name` is what the holder sees when it reads its channels.
|
|
76
|
+
*/
|
|
77
|
+
export const eventsLensSchema = z.strictObject({
|
|
78
|
+
name: z.string().min(1).max(80).optional(),
|
|
79
|
+
where: z.record(z.string(), z.unknown()).optional(),
|
|
80
|
+
select: z.array(integrationEventFieldSchema).min(1).max(20).optional(),
|
|
81
|
+
});
|
|
82
|
+
export type EventsLens = z.infer<typeof eventsLensSchema>;
|
|
83
|
+
|
|
84
|
+
/** What a holder receives from a channel: the event, cut to the channel's `select`. */
|
|
85
|
+
export const selectedEventSchema = integrationEventSchema
|
|
86
|
+
.partial()
|
|
87
|
+
.required({ eventId: true, type: true });
|
|
88
|
+
export type SelectedEvent = z.infer<typeof selectedEventSchema>;
|
|
89
|
+
|
|
90
|
+
/** The event as a channel's `select` lets it through; `eventId` and `type` always go. */
|
|
91
|
+
export const selectEvent = (lens: EventsLens, event: IntegrationEvent): SelectedEvent => {
|
|
92
|
+
if (!lens.select) return event;
|
|
93
|
+
const fields = new Set<IntegrationEventField>([...lens.select, 'eventId', 'type']);
|
|
94
|
+
return Object.fromEntries(
|
|
95
|
+
Object.entries(event).filter(([key]) => fields.has(key as IntegrationEventField)),
|
|
96
|
+
) as SelectedEvent;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** JSON with object keys sorted at every depth: a condition read back from jsonb reorders them. */
|
|
100
|
+
const canonical = (value: unknown): unknown =>
|
|
101
|
+
Array.isArray(value)
|
|
102
|
+
? value.map(canonical)
|
|
103
|
+
: value !== null && typeof value === 'object'
|
|
104
|
+
? Object.fromEntries(
|
|
105
|
+
Object.keys(value)
|
|
106
|
+
.sort()
|
|
107
|
+
.map((key) => [key, canonical((value as Record<string, unknown>)[key])]),
|
|
108
|
+
)
|
|
109
|
+
: value;
|
|
110
|
+
const sameJson = (a: unknown, b: unknown) =>
|
|
111
|
+
JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `granted` reaches nothing `requested` doesn't. Conservative and structural, since this package
|
|
115
|
+
* doesn't evaluate json-rules: the granted `where` is the requested one, or `{all: [...]}` holding
|
|
116
|
+
* it (the requested condition plus more); the granted `select` is within the requested one.
|
|
117
|
+
*/
|
|
118
|
+
export const narrowsEventsLens = (granted: EventsLens, requested: EventsLens) => {
|
|
119
|
+
const whereNarrows =
|
|
120
|
+
requested.where === undefined ||
|
|
121
|
+
sameJson(granted.where, requested.where) ||
|
|
122
|
+
(typeof granted.where === 'object' &&
|
|
123
|
+
granted.where !== null &&
|
|
124
|
+
!Array.isArray(granted.where) &&
|
|
125
|
+
Array.isArray((granted.where as { all?: unknown }).all) &&
|
|
126
|
+
(granted.where as { all: unknown[] }).all.some((part) => sameJson(part, requested.where)));
|
|
127
|
+
const selectNarrows =
|
|
128
|
+
!requested.select ||
|
|
129
|
+
(!!granted.select && granted.select.every((field) => requested.select?.includes(field)));
|
|
130
|
+
return whereNarrows && selectNarrows;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** The operation an `events` resource carries: hold a channel open. It takes no input. */
|
|
134
|
+
export const eventsOperationSchema = z.enum(['events.subscribe']);
|
|
135
|
+
export type EventsOperation = z.infer<typeof eventsOperationSchema>;
|
|
136
|
+
export const eventsOperationNames: Record<EventsOperation, string> = {
|
|
137
|
+
'events.subscribe': "Receive this integration's events as they happen, filtered by the channel",
|
|
138
|
+
};
|
|
139
|
+
export const eventsInputSchemas = {
|
|
140
|
+
'events.subscribe': z.strictObject({}),
|
|
141
|
+
} as const satisfies Record<EventsOperation, z.ZodObject>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A channel's live stream is a Kingdom data stream, like a thread's (`accessThread`): its family is
|
|
145
|
+
* the operationId of Kingdom's snapshot route `GET /api/v1/access/events/{resourceId}/{signetId}`,
|
|
146
|
+
* and its name that route's channel key (path fields sorted). The snapshot is always empty: there is
|
|
147
|
+
* no backlog. Each append's payload is one `SelectedEvent`.
|
|
148
|
+
*/
|
|
149
|
+
export const accessEventsStreamFamily = 'accessEvents';
|
|
150
|
+
export const accessEventsStreamName = (stream: { signetId: string; resourceId: string }) =>
|
|
151
|
+
`${accessEventsStreamFamily}:resourceId:${z.uuid().parse(stream.resourceId)}:signetId:${z.uuid().parse(stream.signetId)}`;
|
|
152
|
+
export const parseAccessEventsStreamName = (name: string) => {
|
|
153
|
+
const [family, resourceField, resourceId, signetField, signetId, ...rest] = name.split(':');
|
|
154
|
+
if (
|
|
155
|
+
family !== accessEventsStreamFamily ||
|
|
156
|
+
resourceField !== 'resourceId' ||
|
|
157
|
+
signetField !== 'signetId'
|
|
158
|
+
)
|
|
159
|
+
return null;
|
|
160
|
+
const parsed = z.object({ signetId: z.uuid(), resourceId: z.uuid() }).safeParse({
|
|
161
|
+
signetId,
|
|
162
|
+
resourceId,
|
|
163
|
+
});
|
|
164
|
+
return parsed.success && rest.length === 0 ? parsed.data : null;
|
|
165
|
+
};
|
|
166
|
+
/** The empty snapshot, in Kingdom's `{ data }` envelope: a channel has no history. */
|
|
167
|
+
export const accessEventsSnapshotSchema = z.object({
|
|
168
|
+
data: z.strictObject({ events: z.tuple([]) }),
|
|
169
|
+
});
|