@kubun/connector 0.17.0 → 0.17.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/lib/authority.d.ts +15 -0
- package/lib/authority.js +19 -0
- package/lib/errors.d.ts +51 -0
- package/lib/errors.js +72 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +2 -0
- package/lib/types.d.ts +228 -13
- package/package.json +3 -3
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability action a caller presents to execute a connector action
|
|
3
|
+
* (create/update) on a `did:kokuin:` owner's behalf. Gated BEFORE any provider
|
|
4
|
+
* credential I/O by the plugin-connector execution path — see
|
|
5
|
+
* `authorizeAction` in `@kubun/engine`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONNECTOR_EXECUTE_ACTION = "connector/execute";
|
|
8
|
+
/**
|
|
9
|
+
* Group-anchored resource specificity tiers for a `connector/execute`
|
|
10
|
+
* capability, most-specific first: the exact model, any model of the
|
|
11
|
+
* connector, then any connector in the group. Every tier is anchored to the
|
|
12
|
+
* group — there is no group-agnostic or global (`*`) tier, so a grant can
|
|
13
|
+
* never authorize a connector spend outside the group it was issued in.
|
|
14
|
+
*/
|
|
15
|
+
export declare function connectorExecuteResourceTiers(groupID: string, connector: string, model: string): Array<string>;
|
package/lib/authority.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability action a caller presents to execute a connector action
|
|
3
|
+
* (create/update) on a `did:kokuin:` owner's behalf. Gated BEFORE any provider
|
|
4
|
+
* credential I/O by the plugin-connector execution path — see
|
|
5
|
+
* `authorizeAction` in `@kubun/engine`.
|
|
6
|
+
*/ export const CONNECTOR_EXECUTE_ACTION = 'connector/execute';
|
|
7
|
+
/**
|
|
8
|
+
* Group-anchored resource specificity tiers for a `connector/execute`
|
|
9
|
+
* capability, most-specific first: the exact model, any model of the
|
|
10
|
+
* connector, then any connector in the group. Every tier is anchored to the
|
|
11
|
+
* group — there is no group-agnostic or global (`*`) tier, so a grant can
|
|
12
|
+
* never authorize a connector spend outside the group it was issued in.
|
|
13
|
+
*/ export function connectorExecuteResourceTiers(groupID, connector, model) {
|
|
14
|
+
return [
|
|
15
|
+
`urn:kubun:connector:${groupID}/${connector}/${model}`,
|
|
16
|
+
`urn:kubun:connector:${groupID}/${connector}/*`,
|
|
17
|
+
`urn:kubun:connector:${groupID}/*`
|
|
18
|
+
];
|
|
19
|
+
}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Machine-readable outcome classification for a failed connector action. */
|
|
2
|
+
export type ConnectorErrorKind = 'rejected' | 'unknown';
|
|
3
|
+
export type ConnectorErrorOptions = ErrorOptions & {
|
|
4
|
+
/** A machine-readable failure code. */
|
|
5
|
+
code?: string;
|
|
6
|
+
/** Whether the failure is a definite non-commit or outcome-unknown. */
|
|
7
|
+
kind?: ConnectorErrorKind;
|
|
8
|
+
/** The provider's HTTP status, when the failure came from a response. */
|
|
9
|
+
httpStatus?: number;
|
|
10
|
+
/** Set when the failure came after a provider write went out. */
|
|
11
|
+
sent?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A connector failure carrying the fields write-back recovery reads: an
|
|
15
|
+
* optional `code`, the outcome classification (`kind`), the provider HTTP
|
|
16
|
+
* status and whether the write already went out (`sent`). A field left unset
|
|
17
|
+
* reads `undefined`, which recovery treats as unclassified.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ConnectorError extends Error {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(message: string, options?: ConnectorErrorOptions);
|
|
22
|
+
get code(): string | undefined;
|
|
23
|
+
get kind(): ConnectorErrorKind | undefined;
|
|
24
|
+
get httpStatus(): number | undefined;
|
|
25
|
+
get sent(): boolean | undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify an HTTP failure by whether the write definitely did not commit.
|
|
29
|
+
* A 4xx (except 429 and 412) is a definite non-commit — the request was
|
|
30
|
+
* rejected and a retry will be rejected the same way. A 429 rate-limit, any
|
|
31
|
+
* 5xx, and any transport failure are outcome-unknown: the provider may have
|
|
32
|
+
* accepted the write before the failure surfaced, so a blind retry risks a
|
|
33
|
+
* duplicate. A 412 (If-Match precondition failed) is also outcome-unknown —
|
|
34
|
+
* it means the provider record changed under us, so recovery must reconcile
|
|
35
|
+
* rather than treat it as a terminal rejection.
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyHTTPStatus(status: number): ConnectorErrorKind;
|
|
38
|
+
/**
|
|
39
|
+
* The error a connector throws for a failed provider HTTP call, tagged with the
|
|
40
|
+
* HTTP status and its outcome classification (`kind`) so the caller can tell a
|
|
41
|
+
* definite rejection from an indeterminate failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function connectorHTTPError(message: string, status: number): ConnectorError;
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a failure raised after a handler's provider write already went out
|
|
46
|
+
* (e.g. the read-back that follows a send). The write may have landed, so the
|
|
47
|
+
* failure is marked `sent` — never taken for a request that could not have
|
|
48
|
+
* reached the provider, and so never refunded — and classified `unknown`, never
|
|
49
|
+
* a definite rejection, whatever the later call's own status was.
|
|
50
|
+
*/
|
|
51
|
+
export declare function markWriteSent(cause: unknown): ConnectorError;
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Machine-readable outcome classification for a failed connector action. */ /**
|
|
2
|
+
* A connector failure carrying the fields write-back recovery reads: an
|
|
3
|
+
* optional `code`, the outcome classification (`kind`), the provider HTTP
|
|
4
|
+
* status and whether the write already went out (`sent`). A field left unset
|
|
5
|
+
* reads `undefined`, which recovery treats as unclassified.
|
|
6
|
+
*/ export class ConnectorError extends Error {
|
|
7
|
+
#code;
|
|
8
|
+
#kind;
|
|
9
|
+
#httpStatus;
|
|
10
|
+
#sent;
|
|
11
|
+
constructor(message, options = {}){
|
|
12
|
+
const { code, kind, httpStatus, sent, ...errorOptions } = options;
|
|
13
|
+
super(message, errorOptions);
|
|
14
|
+
this.name = 'ConnectorError';
|
|
15
|
+
this.#code = code;
|
|
16
|
+
this.#kind = kind;
|
|
17
|
+
this.#httpStatus = httpStatus;
|
|
18
|
+
this.#sent = sent;
|
|
19
|
+
}
|
|
20
|
+
get code() {
|
|
21
|
+
return this.#code;
|
|
22
|
+
}
|
|
23
|
+
get kind() {
|
|
24
|
+
return this.#kind;
|
|
25
|
+
}
|
|
26
|
+
get httpStatus() {
|
|
27
|
+
return this.#httpStatus;
|
|
28
|
+
}
|
|
29
|
+
get sent() {
|
|
30
|
+
return this.#sent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Classify an HTTP failure by whether the write definitely did not commit.
|
|
35
|
+
* A 4xx (except 429 and 412) is a definite non-commit — the request was
|
|
36
|
+
* rejected and a retry will be rejected the same way. A 429 rate-limit, any
|
|
37
|
+
* 5xx, and any transport failure are outcome-unknown: the provider may have
|
|
38
|
+
* accepted the write before the failure surfaced, so a blind retry risks a
|
|
39
|
+
* duplicate. A 412 (If-Match precondition failed) is also outcome-unknown —
|
|
40
|
+
* it means the provider record changed under us, so recovery must reconcile
|
|
41
|
+
* rather than treat it as a terminal rejection.
|
|
42
|
+
*/ export function classifyHTTPStatus(status) {
|
|
43
|
+
return status >= 400 && status < 500 && status !== 429 && status !== 412 ? 'rejected' : 'unknown';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The error a connector throws for a failed provider HTTP call, tagged with the
|
|
47
|
+
* HTTP status and its outcome classification (`kind`) so the caller can tell a
|
|
48
|
+
* definite rejection from an indeterminate failure.
|
|
49
|
+
*/ export function connectorHTTPError(message, status) {
|
|
50
|
+
return new ConnectorError(message, {
|
|
51
|
+
httpStatus: status,
|
|
52
|
+
kind: classifyHTTPStatus(status)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wraps a failure raised after a handler's provider write already went out
|
|
57
|
+
* (e.g. the read-back that follows a send). The write may have landed, so the
|
|
58
|
+
* failure is marked `sent` — never taken for a request that could not have
|
|
59
|
+
* reached the provider, and so never refunded — and classified `unknown`, never
|
|
60
|
+
* a definite rejection, whatever the later call's own status was.
|
|
61
|
+
*/ export function markWriteSent(cause) {
|
|
62
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
63
|
+
const status = cause?.httpStatus;
|
|
64
|
+
return new ConnectorError(message, {
|
|
65
|
+
cause,
|
|
66
|
+
sent: true,
|
|
67
|
+
kind: 'unknown',
|
|
68
|
+
...typeof status === 'number' ? {
|
|
69
|
+
httpStatus: status
|
|
70
|
+
} : {}
|
|
71
|
+
});
|
|
72
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { CONNECTOR_EXECUTE_ACTION, connectorExecuteResourceTiers } from './authority.js';
|
|
1
2
|
export { clusterModelURNs, resolveModelID } from './cluster-utils.js';
|
|
3
|
+
export { ConnectorError, type ConnectorErrorKind, type ConnectorErrorOptions, classifyHTTPStatus, connectorHTTPError, markWriteSent, } from './errors.js';
|
|
2
4
|
export { connectorClusters, connectorModelIDs } from './models/cluster.js';
|
|
3
5
|
export { externalEntityDefinition } from './models/external-entity.js';
|
|
4
|
-
export type { ActionDefinition, ActionHandler, ActionHandlerFactory, ActionResult, ClientDataProvider, ClientProviderFactory, ConnectorAuth, ConnectorDefinition, ConnectorSyncEventPayload, DataProvider, DeviceConnectorAuth, DevicePermission, EntityBatch, EntityRecord, FetchChangesParams, FetchParams, OAuthConnectorAuth, OAuthState, ServerDataProvider, ServerProviderFactory, SyncBoundary, SyncCompleteEvent, SyncErrorEvent, SyncEvent, SyncFailureSample, SyncProgressEvent, SyncState, SyncStateEntry, SyncStateStore, SyncStatus, UserSyncPreferences, WebhookParams, WebhookRegistration, WriteAttachment, } from './types.js';
|
|
6
|
+
export type { ActionDefinition, ActionHandler, ActionHandlerFactory, ActionResult, ActionTargetResolver, ActionTargetStorage, ActionWriteContext, ActionWriteResult, ClientDataProvider, ClientProviderFactory, ConnectorAuth, ConnectorDefinition, ConnectorSyncEventPayload, CreatedSourceIDPredictor, DataProvider, DeadLetter, DeviceConnectorAuth, DevicePermission, EntityBatch, EntityRecord, FetchChangesParams, FetchedAbsentEntity, FetchedEntity, FetchedPresentEntity, FetchParams, IntentCanonicalizer, IntentComparison, JSONValue, LocalAnnotationMerger, OAuthConnectorAuth, OAuthState, PendingOp, ReconcileResult, Recovery, ResolvedActionTarget, ServerDataProvider, ServerProviderFactory, SyncBoundary, SyncCompleteEvent, SyncErrorEvent, SyncEvent, SyncFailureSample, SyncProgressCASCurrent, SyncProgressCASResult, SyncProgressEvent, SyncProgressRecompute, SyncState, SyncStateEntry, SyncStateStore, SyncStatus, UserSyncPreferences, WebhookParams, WebhookRegistration, WriteAttachment, } from './types.js';
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { CONNECTOR_EXECUTE_ACTION, connectorExecuteResourceTiers } from './authority.js';
|
|
1
2
|
export { clusterModelURNs, resolveModelID } from './cluster-utils.js';
|
|
3
|
+
export { ConnectorError, classifyHTTPStatus, connectorHTTPError, markWriteSent } from './errors.js';
|
|
2
4
|
export { connectorClusters, connectorModelIDs } from './models/cluster.js';
|
|
3
5
|
export { externalEntityDefinition } from './models/external-entity.js';
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credential } from '@kubun/credential-types';
|
|
1
|
+
import type { Credential, ProviderAccount } from '@kubun/credential-types';
|
|
2
2
|
import type { ClustersRecord } from '@kubun/protocol';
|
|
3
3
|
import type { JSONSchema } from 'json-schema-typed';
|
|
4
4
|
export type OAuthState = {
|
|
@@ -9,6 +9,37 @@ export type OAuthState = {
|
|
|
9
9
|
createdAt: number;
|
|
10
10
|
};
|
|
11
11
|
export type SyncStatus = 'idle' | 'syncing' | 'error';
|
|
12
|
+
export type JSONValue = string | number | boolean | null | {
|
|
13
|
+
[key: string]: JSONValue;
|
|
14
|
+
} | Array<JSONValue>;
|
|
15
|
+
export type PendingOp = {
|
|
16
|
+
docID: string;
|
|
17
|
+
kind: 'upsert' | 'delete';
|
|
18
|
+
payload: JSONValue;
|
|
19
|
+
attempts: number;
|
|
20
|
+
firstFailedAt: string;
|
|
21
|
+
lastError: {
|
|
22
|
+
code?: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type DeadLetter = {
|
|
27
|
+
docID: string | null;
|
|
28
|
+
sourceService: string;
|
|
29
|
+
sourceID: string;
|
|
30
|
+
kind: 'upsert' | 'delete';
|
|
31
|
+
attempts: number;
|
|
32
|
+
lastError: {
|
|
33
|
+
code?: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
deadLetteredAt: string;
|
|
37
|
+
reason: 'max-attempts' | 'payload-evicted' | 'unserializable' | 'unaddressable';
|
|
38
|
+
};
|
|
39
|
+
export type Recovery = {
|
|
40
|
+
pending: Array<PendingOp>;
|
|
41
|
+
deadLetters: Array<DeadLetter>;
|
|
42
|
+
};
|
|
12
43
|
export type SyncState = {
|
|
13
44
|
checkpoint: unknown;
|
|
14
45
|
lastSyncedAt: string;
|
|
@@ -16,19 +47,33 @@ export type SyncState = {
|
|
|
16
47
|
status: SyncStatus;
|
|
17
48
|
error?: string;
|
|
18
49
|
leaseExpiresAt?: number | null;
|
|
50
|
+
recovery?: Recovery | null;
|
|
51
|
+
version?: number;
|
|
52
|
+
incarnation?: string;
|
|
19
53
|
};
|
|
20
54
|
export type SyncStateEntry = SyncState & {
|
|
21
55
|
connectorName: string;
|
|
22
56
|
ownerDID: string;
|
|
23
57
|
};
|
|
58
|
+
export type SyncProgressCASCurrent = {
|
|
59
|
+
checkpoint: unknown;
|
|
60
|
+
recovery: Recovery | null;
|
|
61
|
+
version: number;
|
|
62
|
+
incarnation: string;
|
|
63
|
+
};
|
|
64
|
+
export type SyncProgressCASResult = {
|
|
65
|
+
checkpoint: unknown;
|
|
66
|
+
recovery: Recovery | null;
|
|
67
|
+
lastSyncedAt: string;
|
|
68
|
+
entityCount: number;
|
|
69
|
+
error?: string | null;
|
|
70
|
+
};
|
|
71
|
+
export type SyncProgressRecompute = (current: SyncProgressCASCurrent | null) => SyncProgressCASResult | Promise<SyncProgressCASResult>;
|
|
24
72
|
export type SyncStateStore = {
|
|
25
73
|
get(connectorName: string, ownerDID: string): Promise<SyncState | null>;
|
|
26
74
|
set(connectorName: string, ownerDID: string, state: SyncState): Promise<void>;
|
|
27
|
-
updateSyncProgress(connectorName: string, ownerDID: string,
|
|
28
|
-
|
|
29
|
-
lastSyncedAt: string;
|
|
30
|
-
entityCount: number;
|
|
31
|
-
error?: string;
|
|
75
|
+
updateSyncProgress(connectorName: string, ownerDID: string, recompute: SyncProgressRecompute, options?: {
|
|
76
|
+
maxRetries?: number;
|
|
32
77
|
}): Promise<void>;
|
|
33
78
|
delete(connectorName: string, ownerDID: string): Promise<void>;
|
|
34
79
|
listForConnector(connectorName: string): Promise<Array<SyncStateEntry>>;
|
|
@@ -48,13 +93,25 @@ export type EntityRecord = Record<string, unknown> & {
|
|
|
48
93
|
sourceService: string;
|
|
49
94
|
sourceID: string;
|
|
50
95
|
};
|
|
96
|
+
export type FetchedEntity = {
|
|
97
|
+
present: true;
|
|
98
|
+
record: EntityRecord;
|
|
99
|
+
sourceFetchHLC: string;
|
|
100
|
+
} | {
|
|
101
|
+
present: false;
|
|
102
|
+
sourceID: string;
|
|
103
|
+
sourceFetchHLC: string;
|
|
104
|
+
};
|
|
105
|
+
export type FetchedPresentEntity = Extract<FetchedEntity, {
|
|
106
|
+
present: true;
|
|
107
|
+
}>;
|
|
108
|
+
export type FetchedAbsentEntity = Extract<FetchedEntity, {
|
|
109
|
+
present: false;
|
|
110
|
+
}>;
|
|
51
111
|
export type EntityBatch = {
|
|
52
112
|
modelID: string;
|
|
53
|
-
entities: Array<
|
|
54
|
-
deleted?: Array<
|
|
55
|
-
sourceService: string;
|
|
56
|
-
sourceID: string;
|
|
57
|
-
}>;
|
|
113
|
+
entities: Array<FetchedPresentEntity>;
|
|
114
|
+
deleted?: Array<FetchedAbsentEntity>;
|
|
58
115
|
checkpoint?: unknown;
|
|
59
116
|
hasMore: boolean;
|
|
60
117
|
};
|
|
@@ -66,9 +123,30 @@ export type FetchParams = {
|
|
|
66
123
|
export type FetchChangesParams = FetchParams & {
|
|
67
124
|
lastSyncedAt?: Date;
|
|
68
125
|
};
|
|
126
|
+
export type ReconcileResult = (FetchedPresentEntity & {
|
|
127
|
+
providerETag?: string;
|
|
128
|
+
}) | (FetchedAbsentEntity & {
|
|
129
|
+
providerETag?: string;
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* A source of external entities, driven by an opaque `checkpoint`.
|
|
133
|
+
*
|
|
134
|
+
* **Replay-stability contract (required).** Re-opening `fetchAll`/`fetchChanges`
|
|
135
|
+
* from the same `checkpoint` MUST re-emit the same operations — or a superset —
|
|
136
|
+
* addressed by `sourceService`+`sourceID`; it must never emit a narrower set. The
|
|
137
|
+
* sync engine advances the connector checkpoint before the durable workflow
|
|
138
|
+
* checkpoint, so a crash between the two re-drives `fetchChanges`/`fetchAll` from
|
|
139
|
+
* an older checkpoint; a stable replay makes that re-drive idempotent (imports
|
|
140
|
+
* upsert by unique key) with no dropped entities. Concretely: a time window MUST
|
|
141
|
+
* be an absolute bound carried in the checkpoint, never recomputed from `now` on
|
|
142
|
+
* each call — a `now`-relative window silently narrows on replay. (Server-side
|
|
143
|
+
* ordering may vary between calls: harmless, since ops are keyed by identity.)
|
|
144
|
+
*/
|
|
69
145
|
export type DataProvider = {
|
|
70
146
|
fetchAll(params: FetchParams): AsyncIterable<EntityBatch>;
|
|
71
147
|
fetchChanges(params: FetchChangesParams): AsyncIterable<EntityBatch>;
|
|
148
|
+
fetchEntity(model: string, sourceID: string, snapshot: unknown): Promise<FetchedEntity>;
|
|
149
|
+
reconcile(model: string, sourceID: string, snapshot: unknown): Promise<ReconcileResult>;
|
|
72
150
|
supportedStrategies: Array<'polling' | 'on-demand'>;
|
|
73
151
|
};
|
|
74
152
|
export type WebhookParams = {
|
|
@@ -107,6 +185,20 @@ export type ActionDefinition = {
|
|
|
107
185
|
output?: JSONSchema.Object;
|
|
108
186
|
entityModel?: string;
|
|
109
187
|
requiresCredential: boolean;
|
|
188
|
+
recoveryClass: 'reissue-safe' | 'reconcile-only';
|
|
189
|
+
/**
|
|
190
|
+
* The field of the target document holding the provider's conditional-write
|
|
191
|
+
* token (e.g. an ETag) as last fetched. An update pins it on the operation
|
|
192
|
+
* at claim — read from the local document, never re-fetched before the
|
|
193
|
+
* write — and hands it to the handler as `ActionWriteContext.providerETag`.
|
|
194
|
+
*/
|
|
195
|
+
providerETagField?: string;
|
|
196
|
+
/**
|
|
197
|
+
* The provider writes only against that token: an update whose target
|
|
198
|
+
* document holds none is refused before the operation is claimed (a sync
|
|
199
|
+
* first provides it), rather than claimed and then dead-lettered.
|
|
200
|
+
*/
|
|
201
|
+
providerETagRequired?: boolean;
|
|
110
202
|
};
|
|
111
203
|
export type ActionResult = {
|
|
112
204
|
raw?: Record<string, unknown>;
|
|
@@ -119,21 +211,123 @@ export type WriteAttachment = (input: ReadableStream<Uint8Array>, options?: {
|
|
|
119
211
|
encrypted?: boolean;
|
|
120
212
|
keyID?: string;
|
|
121
213
|
}) => Promise<string>;
|
|
214
|
+
export type ActionWriteResult = FetchedPresentEntity & {
|
|
215
|
+
providerETag?: string;
|
|
216
|
+
localChange?: boolean;
|
|
217
|
+
};
|
|
218
|
+
/** What the write-back layer knows about the operation a write belongs to. */
|
|
219
|
+
export type ActionWriteContext = {
|
|
220
|
+
/**
|
|
221
|
+
* The caller-derived deterministic identity of the intent the write carries
|
|
222
|
+
* (the operation envelope's `deterministicEventID`), the same on every
|
|
223
|
+
* attempt of the operation. A connector whose provider accepts a
|
|
224
|
+
* caller-chosen resource id derives that id from it, so a create re-issued
|
|
225
|
+
* after a lost response collides with its own earlier effect instead of
|
|
226
|
+
* duplicating it.
|
|
227
|
+
*/
|
|
228
|
+
deterministicEventID: string;
|
|
229
|
+
/**
|
|
230
|
+
* The provider's conditional-write token (e.g. an ETag) for the state the
|
|
231
|
+
* write is based on, as captured when that state was fetched and pinned on
|
|
232
|
+
* the operation at claim (see `ActionDefinition.providerETagField`). An
|
|
233
|
+
* update writes conditionally on it, so a provider change since then fails
|
|
234
|
+
* the write instead of being overwritten. Absent when unknown.
|
|
235
|
+
*/
|
|
236
|
+
providerETag?: string | null;
|
|
237
|
+
};
|
|
122
238
|
export type ActionHandler = {
|
|
123
|
-
create(model: string, params: Record<string, unknown
|
|
124
|
-
update(model: string, sourceID: string, params: Record<string, unknown>, existing?: Record<string, unknown
|
|
239
|
+
create(model: string, params: Record<string, unknown>, context?: ActionWriteContext): Promise<ActionWriteResult>;
|
|
240
|
+
update(model: string, sourceID: string, params: Record<string, unknown>, existing?: Record<string, unknown>, context?: ActionWriteContext): Promise<ActionWriteResult>;
|
|
125
241
|
};
|
|
126
242
|
export type ActionHandlerFactory = (config: {
|
|
127
243
|
credential: Credential;
|
|
128
244
|
writeAttachment?: WriteAttachment;
|
|
245
|
+
mintFetchHLC: () => string;
|
|
129
246
|
}) => ActionHandler;
|
|
130
247
|
export type ServerProviderFactory = (config: {
|
|
131
248
|
credential: unknown;
|
|
132
249
|
boundary: SyncBoundary;
|
|
250
|
+
mintFetchHLC: () => string;
|
|
133
251
|
}) => ServerDataProvider;
|
|
134
252
|
export type ClientProviderFactory = (config: {
|
|
135
253
|
boundary: SyncBoundary;
|
|
136
254
|
}) => ClientDataProvider;
|
|
255
|
+
/** A write's intent and a provider record, in the forms they are compared in. */
|
|
256
|
+
export type IntentComparison = {
|
|
257
|
+
intent: Record<string, unknown>;
|
|
258
|
+
record: Record<string, unknown>;
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Brings a write's intent and a provider record into the forms the provider
|
|
262
|
+
* normalizes to, so a record that holds the intent compares equal field by
|
|
263
|
+
* field (e.g. a date-time the provider re-renders in another offset). Only an
|
|
264
|
+
* equivalence the provider guarantees may be folded in: anything looser would
|
|
265
|
+
* confirm an effect that never happened. `null` means the record cannot hold
|
|
266
|
+
* the intent at all.
|
|
267
|
+
*/
|
|
268
|
+
export type IntentCanonicalizer = (params: {
|
|
269
|
+
model: string;
|
|
270
|
+
op: 'create' | 'update';
|
|
271
|
+
intent: Record<string, unknown>;
|
|
272
|
+
record: EntityRecord;
|
|
273
|
+
}) => IntentComparison | null;
|
|
274
|
+
/**
|
|
275
|
+
* Carries the device-local annotations of an existing document into an
|
|
276
|
+
* incoming provider record that does not author them — e.g. an imported
|
|
277
|
+
* attachment's `blob`, nested in attachment metadata the provider does author
|
|
278
|
+
* — so a later provider write of the entity keeps them. Only local
|
|
279
|
+
* annotations may be carried over, never a provider-authored value.
|
|
280
|
+
*/
|
|
281
|
+
export type LocalAnnotationMerger = (params: {
|
|
282
|
+
model: string;
|
|
283
|
+
incoming: EntityRecord;
|
|
284
|
+
existing: Record<string, unknown>;
|
|
285
|
+
}) => EntityRecord;
|
|
286
|
+
/** Storage-only reads available to an {@link ActionTargetResolver}. */
|
|
287
|
+
export type ActionTargetStorage = {
|
|
288
|
+
/**
|
|
289
|
+
* The acting owner's live documents of one of this connector's models, as
|
|
290
|
+
* synced to this device (document data only). Never a provider call.
|
|
291
|
+
*/
|
|
292
|
+
documents(model: string): Promise<Array<Record<string, unknown>>>;
|
|
293
|
+
};
|
|
294
|
+
/** The canonical target of a write and the input it is written with. */
|
|
295
|
+
export type ResolvedActionTarget = {
|
|
296
|
+
/** The canonical provider target the write lands in (pinned on the operation). */
|
|
297
|
+
target: string;
|
|
298
|
+
/** The action input with any alias replaced by what it resolved to. */
|
|
299
|
+
input: Record<string, unknown>;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Derives the canonical target of a write from its own action input, before
|
|
303
|
+
* the operation is claimed and with storage-only access: an alias (a
|
|
304
|
+
* provider's "primary", say) is resolved from locally synced data, never by a
|
|
305
|
+
* provider call. The resolved input is what the operation pins and what the
|
|
306
|
+
* handler writes with. Throws a coded error when the target cannot be
|
|
307
|
+
* resolved locally.
|
|
308
|
+
*/
|
|
309
|
+
export type ActionTargetResolver = (params: {
|
|
310
|
+
model: string;
|
|
311
|
+
op: 'create' | 'update';
|
|
312
|
+
input: Record<string, unknown>;
|
|
313
|
+
sourceID?: string;
|
|
314
|
+
/** The provider account of the stored credential the write will spend. */
|
|
315
|
+
providerAccount: ProviderAccount;
|
|
316
|
+
storage: ActionTargetStorage;
|
|
317
|
+
}) => Promise<ResolvedActionTarget>;
|
|
318
|
+
/**
|
|
319
|
+
* Predicts, from a create's resolved input and its operation's deterministic
|
|
320
|
+
* event id alone (no storage, no provider call), the provider id the created
|
|
321
|
+
* entity will be stored under — for a provider whose create inserts under a
|
|
322
|
+
* caller-chosen id. `null` when the provider assigns the id itself, or when
|
|
323
|
+
* the input does not determine it. Every attempt of the operation must end
|
|
324
|
+
* up under the predicted id.
|
|
325
|
+
*/
|
|
326
|
+
export type CreatedSourceIDPredictor = (params: {
|
|
327
|
+
model: string;
|
|
328
|
+
input: Record<string, unknown>;
|
|
329
|
+
deterministicEventID: string;
|
|
330
|
+
}) => string | null;
|
|
137
331
|
export type ConnectorDefinition = {
|
|
138
332
|
name: string;
|
|
139
333
|
version: string;
|
|
@@ -141,6 +335,27 @@ export type ConnectorDefinition = {
|
|
|
141
335
|
syncStrategies: Array<'polling' | 'webhook' | 'on-demand'>;
|
|
142
336
|
actions?: Array<ActionDefinition>;
|
|
143
337
|
actionHandler?: ActionHandlerFactory;
|
|
338
|
+
/**
|
|
339
|
+
* How write-back recovery compares an operation's intent against the
|
|
340
|
+
* provider resource that should hold it. Absent, values compare exactly.
|
|
341
|
+
*/
|
|
342
|
+
canonicalizeIntent?: IntentCanonicalizer;
|
|
343
|
+
/**
|
|
344
|
+
* Applied to every provider record projected onto an existing document
|
|
345
|
+
* (sync, verification re-pull, an action's write result).
|
|
346
|
+
*/
|
|
347
|
+
mergeLocalAnnotations?: LocalAnnotationMerger;
|
|
348
|
+
/**
|
|
349
|
+
* How a write's canonical target is derived from its input. Absent, the
|
|
350
|
+
* caller must name it as a generic `input.target`.
|
|
351
|
+
*/
|
|
352
|
+
resolveActionTarget?: ActionTargetResolver;
|
|
353
|
+
/**
|
|
354
|
+
* The provider id a create will be stored under, when the connector chooses
|
|
355
|
+
* it (see {@link CreatedSourceIDPredictor}). Absent, a create's document is
|
|
356
|
+
* known only once the write commits.
|
|
357
|
+
*/
|
|
358
|
+
predictCreatedSourceID?: CreatedSourceIDPredictor;
|
|
144
359
|
serverProvider?: ServerProviderFactory;
|
|
145
360
|
clientProvider?: ClientProviderFactory;
|
|
146
361
|
graphql?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/connector",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/credential-types": "^0.17.
|
|
19
|
-
"@kubun/protocol": "^0.17.
|
|
18
|
+
"@kubun/credential-types": "^0.17.2",
|
|
19
|
+
"@kubun/protocol": "^0.17.1",
|
|
20
20
|
"@kubun/scalars": "^0.17.0",
|
|
21
21
|
"json-schema-typed": "^8.0.2"
|
|
22
22
|
},
|