@kubun/connector 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,57 @@
1
+ # The Prosperity Public License 3.0.0
2
+
3
+ Contributor: Paul Le Cam
4
+
5
+ Source Code: https://github.com/PaulLeCam/kubun
6
+
7
+ ## Purpose
8
+
9
+ This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
10
+
11
+ ## Agreement
12
+
13
+ In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
14
+
15
+ ## Notices
16
+
17
+ Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
18
+
19
+ ## Commercial Trial
20
+
21
+ Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
22
+
23
+ ## Contributions Back
24
+
25
+ Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
26
+
27
+ ## Personal Uses
28
+
29
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
30
+
31
+ ## Noncommercial Organizations
32
+
33
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
34
+
35
+ ## Defense
36
+
37
+ Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
38
+
39
+ ## Copyright
40
+
41
+ The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
42
+
43
+ ## Patent
44
+
45
+ The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
46
+
47
+ ## Reliability
48
+
49
+ The contributor can't revoke this license.
50
+
51
+ ## Excuse
52
+
53
+ You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
54
+
55
+ ## No Liability
56
+
57
+ ***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
package/lib/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export { type ConnectorActivatedEvent, type ConnectorDeactivatedEvent, ConnectorManager, type ConnectorManagerEvents, type ConnectorManagerParams, } from './manager.js';
2
+ export { externalEntityDefinition } from './models/external-entity.js';
3
+ export { createOAuthRoutes, type OAuthRoutesParams, type StateStore } from './oauth/routes.js';
4
+ export type { Credential, CredentialProvider, OAuthProviderDefinition, OAuthState, TokenResponse, } from './oauth/types.js';
5
+ export { type ConnectorActionParams, type ConnectorActionResult, type ConnectorAuthCompleteParams, type ConnectorAuthCompleteResult, type ConnectorAuthURLParams, type ConnectorAuthURLResult, type ConnectorProtocol, type ConnectorPushCompleteParams, type ConnectorPushCompleteResult, type ConnectorPushParams, type ConnectorPushResult, type ConnectorStatusParams, type ConnectorStatusResult, type ConnectorSyncParams, type ConnectorSyncReceive, type ConnectorSyncResult, connectorProtocol, } from './protocol.js';
6
+ export { ConnectorRegistry } from './registry.js';
7
+ export { computeEffectiveBoundary, isWithinBoundary, parseDuration, } from './sync/boundary.js';
8
+ export type { ActionDefinition, ActionResult, ClientDataProvider, ClientProviderFactory, ConnectorAuth, ConnectorDefinition, DataProvider, DeviceConnectorAuth, DevicePermission, EntityBatch, EntityRecord, FetchChangesParams, FetchParams, OAuthConnectorAuth, ServerDataProvider, ServerProviderFactory, SyncBoundary, SyncCompleteEvent, SyncErrorEvent, SyncEvent, SyncProgressEvent, UserSyncPreferences, WebhookParams, WebhookRegistration, } from './types.js';
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ export{ConnectorManager}from"./manager.js";export{externalEntityDefinition}from"./models/external-entity.js";export{createOAuthRoutes}from"./oauth/routes.js";export{connectorProtocol}from"./protocol.js";export{ConnectorRegistry}from"./registry.js";export{computeEffectiveBoundary,isWithinBoundary,parseDuration}from"./sync/boundary.js";
@@ -0,0 +1,37 @@
1
+ import type { Logger } from '@kubun/logger';
2
+ import type { ConnectorRegistry } from './registry.js';
3
+ import type { SyncBoundary } from './types.js';
4
+ export type ConnectorActivatedEvent = {
5
+ connectorName: string;
6
+ graphID: string;
7
+ };
8
+ export type ConnectorDeactivatedEvent = {
9
+ connectorName: string;
10
+ };
11
+ export type ConnectorManagerEvents = {
12
+ 'connector:activated': ConnectorActivatedEvent;
13
+ 'connector:deactivated': ConnectorDeactivatedEvent;
14
+ };
15
+ export type ConnectorManagerParams = {
16
+ registry: ConnectorRegistry;
17
+ logger: Logger;
18
+ defaults?: {
19
+ boundary?: SyncBoundary;
20
+ pollingInterval?: string;
21
+ };
22
+ };
23
+ export declare class ConnectorManager {
24
+ #private;
25
+ constructor(params: ConnectorManagerParams);
26
+ get defaults(): {
27
+ boundary: SyncBoundary;
28
+ pollingInterval: string;
29
+ };
30
+ activateForGraph(graphID: string, connectorNames: Array<string>): void;
31
+ deactivateForGraph(graphID: string, connectorNames: Array<string>): void;
32
+ isActive(connectorName: string): boolean;
33
+ getActiveGraphs(connectorName: string): Array<string>;
34
+ listActive(): Array<string>;
35
+ onConnectorActivated(callback: (event: ConnectorActivatedEvent) => void): () => void;
36
+ onConnectorDeactivated(callback: (event: ConnectorDeactivatedEvent) => void): () => void;
37
+ }
package/lib/manager.js ADDED
@@ -0,0 +1 @@
1
+ import{EventEmitter as e}from"@enkaku/event";export class ConnectorManager{#e;#t;#r=new Map;#a=new e;#i;constructor(e){this.#e=e.registry,this.#t=e.logger,this.#i={boundary:e.defaults?.boundary??{maxAge:"P90D"},pollingInterval:e.defaults?.pollingInterval??"PT5M"}}get defaults(){return this.#i}activateForGraph(e,t){for(let r of t){if(!this.#e.has(r))throw Error(`Connector "${r}" is not registered`);let t=this.#r.get(r),a=null==t||0===t.size;null==t&&(t=new Set,this.#r.set(r,t)),t.add(e),a&&(this.#t.info("Connector {name} activated by graph {graphID}",{name:r,graphID:e}),this.#a.emit("connector:activated",{connectorName:r,graphID:e}))}}deactivateForGraph(e,t){for(let r of t){let t=this.#r.get(r);null!=t&&(t.delete(e),0===t.size&&(this.#r.delete(r),this.#t.info("Connector {name} deactivated",{name:r}),this.#a.emit("connector:deactivated",{connectorName:r})))}}isActive(e){let t=this.#r.get(e);return null!=t&&t.size>0}getActiveGraphs(e){let t=this.#r.get(e);return t?Array.from(t):[]}listActive(){return Array.from(this.#r.keys()).filter(e=>this.isActive(e))}onConnectorActivated(e){return this.#a.on("connector:activated",e)}onConnectorDeactivated(e){return this.#a.on("connector:deactivated",e)}}
@@ -0,0 +1,11 @@
1
+ import type { DocumentModelInput } from '@kubun/protocol';
2
+ /**
3
+ * ExternalEntity is the base interface for documents synced from external services.
4
+ *
5
+ * Fields:
6
+ * - sourceService: External service identifier (e.g., "jira", "slack", "ios-contacts")
7
+ * - sourceID: External entity native ID
8
+ * - sourceURL: Optional URL back to the entity in the source service
9
+ * - lastSyncedAt: Timestamp of when this entity was last synced
10
+ */
11
+ export declare const externalEntityDefinition: DocumentModelInput;
@@ -0,0 +1 @@
1
+ import{dateTimeSchema as e,urlSchema as i}from"@kubun/scalars";export const externalEntityDefinition={name:"ExternalEntity",behavior:"interface",schema:{type:"object",properties:{sourceService:{$ref:"#/definitions/SourceService"},sourceID:{$ref:"#/definitions/SourceID"},sourceURL:{$ref:"#/definitions/SourceURL"},lastSyncedAt:{$ref:"#/definitions/DateTime"}},required:["sourceService","sourceID","lastSyncedAt"],additionalProperties:!0,definitions:{SourceService:{type:"string",title:"SourceService",minLength:1,maxLength:50},SourceID:{type:"string",title:"SourceID",minLength:1,maxLength:500},SourceURL:i,DateTime:e}},fieldsMeta:{}};
@@ -0,0 +1,19 @@
1
+ import { Hono } from 'hono';
2
+ import type { Credential, OAuthProviderDefinition, OAuthState } from './types.js';
3
+ export type StateStore = {
4
+ set(nonce: string, state: OAuthState): Promise<void> | void;
5
+ get(nonce: string): Promise<OAuthState | null> | OAuthState | null;
6
+ delete(nonce: string): Promise<void> | void;
7
+ };
8
+ export type OAuthRoutesParams = {
9
+ providers: Array<OAuthProviderDefinition>;
10
+ stateStore: StateStore;
11
+ /** Maximum age of OAuth state in milliseconds (default: 10 minutes) */
12
+ stateTTL?: number;
13
+ onTokenReceived?: (params: {
14
+ provider: OAuthProviderDefinition;
15
+ state: OAuthState;
16
+ credential: Credential;
17
+ }) => Promise<void> | void;
18
+ };
19
+ export declare function createOAuthRoutes(params: OAuthRoutesParams): Hono;
@@ -0,0 +1 @@
1
+ import{Hono as e}from"hono";export function createOAuthRoutes(r){let{providers:t,stateStore:a,stateTTL:i=6e5,onTokenReceived:o}=r,n=new Map(t.map(e=>[e.name,e])),s=new e;return s.get("/auth/:provider/start",async e=>{let r,t=e.req.param("provider"),i=n.get(t);if(!i)return e.json({error:`Unknown provider: ${t}`},404);if(!i.clientID)return e.json({error:`Provider ${t} is not configured with a clientID`},500);let o=e.req.query("redirect_uri");if(!o)return e.json({error:"redirect_uri is required"},400);let s=e.req.query("scope"),c=s?s.split(","):[],d=[...i.baseScopes??[],...c],p=(r=new Uint8Array(32),crypto.getRandomValues(r),Array.from(r,e=>e.toString(16).padStart(2,"0")).join("")),u={provider:t,redirectURL:o,nonce:p,createdAt:Date.now()};await a.set(p,u);let l=new URL(i.authorizationEndpoint);return l.searchParams.set("client_id",i.clientID),l.searchParams.set("redirect_uri",o),l.searchParams.set("response_type","code"),l.searchParams.set("state",p),d.length>0&&l.searchParams.set("scope",d.join(" ")),e.redirect(l.toString())}),s.get("/auth/:provider/callback",async e=>{let r=e.req.param("provider"),t=n.get(r);if(!t)return e.json({error:`Unknown provider: ${r}`},404);let s=e.req.query("code"),c=e.req.query("state"),d=e.req.query("error");if(d){let r=e.req.query("error_description");return e.json({error:d,error_description:r},400)}if(!s||!c)return e.json({error:"Missing code or state"},400);let p=await a.get(c);if(!p)return e.json({error:"Invalid or expired state"},400);if(Date.now()-p.createdAt>i)return await a.delete(c),e.json({error:"State has expired"},400);if(p.provider!==r)return e.json({error:"State provider mismatch"},400);let u=await fetch(t.tokenEndpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:s,redirect_uri:p.redirectURL,client_id:t.clientID??"",client_secret:t.clientSecret??""})});if(!u.ok){let r=await u.text();return e.json({error:"Token exchange failed",details:r},500)}let l=await u.json(),h={accessToken:l.access_token,refreshToken:l.refresh_token,expiresAt:l.expires_in?new Date(Date.now()+1e3*l.expires_in):void 0,scopes:l.scope?l.scope.split(" "):[]};await a.delete(c),o&&await o({provider:t,state:p,credential:h});let w=new URL(p.redirectURL);return w.searchParams.set("oauth_status","success"),w.searchParams.set("provider",r),e.redirect(w.toString())}),s}
@@ -0,0 +1,35 @@
1
+ export type OAuthProviderDefinition = {
2
+ name: string;
3
+ authorizationEndpoint: string;
4
+ tokenEndpoint: string;
5
+ baseScopes?: Array<string>;
6
+ clientID?: string;
7
+ clientSecret?: string;
8
+ };
9
+ export type Credential = {
10
+ accessToken: string;
11
+ refreshToken?: string;
12
+ expiresAt?: Date;
13
+ scopes: Array<string>;
14
+ accountLabel?: string;
15
+ metadata?: Record<string, unknown>;
16
+ };
17
+ export type CredentialProvider = {
18
+ get(providerName: string, ownerDID: string): Promise<Credential | null>;
19
+ set(providerName: string, ownerDID: string, credential: Credential): Promise<void>;
20
+ delete(providerName: string, ownerDID: string): Promise<void>;
21
+ };
22
+ export type OAuthState = {
23
+ provider: string;
24
+ redirectURL: string;
25
+ connectors?: Array<string>;
26
+ nonce: string;
27
+ createdAt: number;
28
+ };
29
+ export type TokenResponse = {
30
+ access_token: string;
31
+ refresh_token?: string;
32
+ expires_in?: number;
33
+ token_type: string;
34
+ scope?: string;
35
+ };
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,337 @@
1
+ import type { FromSchema } from '@enkaku/schema';
2
+ export declare const connectorProtocol: {
3
+ readonly 'connector/sync': {
4
+ readonly type: "stream";
5
+ readonly param: {
6
+ readonly type: "object";
7
+ readonly properties: {
8
+ readonly connector: {
9
+ readonly type: "string";
10
+ readonly description: "Connector name";
11
+ };
12
+ readonly full: {
13
+ readonly type: "boolean";
14
+ readonly description: "Force full sync instead of incremental";
15
+ };
16
+ };
17
+ readonly required: readonly ["connector"];
18
+ readonly additionalProperties: false;
19
+ };
20
+ readonly receive: {
21
+ readonly type: "object";
22
+ readonly properties: {
23
+ readonly type: {
24
+ readonly type: "string";
25
+ readonly enum: readonly ["progress", "entity", "error"];
26
+ };
27
+ readonly connectorName: {
28
+ readonly type: "string";
29
+ };
30
+ readonly phase: {
31
+ readonly type: "string";
32
+ readonly enum: readonly ["initial", "incremental"];
33
+ };
34
+ readonly entitiesProcessed: {
35
+ readonly type: "number";
36
+ };
37
+ readonly entitiesFailed: {
38
+ readonly type: "number";
39
+ };
40
+ readonly currentBatch: {
41
+ readonly type: "number";
42
+ };
43
+ readonly checkpoint: {};
44
+ readonly entity: {
45
+ readonly type: "object";
46
+ };
47
+ readonly documentID: {
48
+ readonly type: "string";
49
+ };
50
+ readonly error: {
51
+ readonly type: "string";
52
+ };
53
+ };
54
+ readonly additionalProperties: false;
55
+ };
56
+ readonly result: {
57
+ readonly type: "object";
58
+ readonly properties: {
59
+ readonly success: {
60
+ readonly type: "boolean";
61
+ };
62
+ readonly totalProcessed: {
63
+ readonly type: "number";
64
+ };
65
+ readonly totalFailed: {
66
+ readonly type: "number";
67
+ };
68
+ readonly duration: {
69
+ readonly type: "number";
70
+ };
71
+ };
72
+ readonly required: readonly ["success", "totalProcessed", "totalFailed", "duration"];
73
+ readonly additionalProperties: false;
74
+ };
75
+ };
76
+ readonly 'connector/status': {
77
+ readonly type: "request";
78
+ readonly param: {
79
+ readonly type: "object";
80
+ readonly properties: {
81
+ readonly connector: {
82
+ readonly type: "string";
83
+ readonly description: "Optional connector name to filter";
84
+ };
85
+ };
86
+ readonly additionalProperties: false;
87
+ };
88
+ readonly result: {
89
+ readonly type: "object";
90
+ readonly properties: {
91
+ readonly connectors: {
92
+ readonly type: "array";
93
+ readonly items: {
94
+ readonly type: "object";
95
+ readonly properties: {
96
+ readonly name: {
97
+ readonly type: "string";
98
+ };
99
+ readonly lastSyncedAt: {
100
+ readonly type: "string";
101
+ };
102
+ readonly entityCount: {
103
+ readonly type: "number";
104
+ };
105
+ readonly status: {
106
+ readonly type: "string";
107
+ readonly enum: readonly ["active", "inactive", "syncing", "error"];
108
+ };
109
+ readonly error: {
110
+ readonly type: "string";
111
+ };
112
+ };
113
+ readonly required: readonly ["name", "status"];
114
+ readonly additionalProperties: false;
115
+ };
116
+ };
117
+ };
118
+ readonly required: readonly ["connectors"];
119
+ readonly additionalProperties: false;
120
+ };
121
+ };
122
+ readonly 'connector/action': {
123
+ readonly type: "request";
124
+ readonly param: {
125
+ readonly type: "object";
126
+ readonly properties: {
127
+ readonly connector: {
128
+ readonly type: "string";
129
+ };
130
+ readonly action: {
131
+ readonly type: "string";
132
+ };
133
+ readonly input: {
134
+ readonly type: "object";
135
+ };
136
+ };
137
+ readonly required: readonly ["connector", "action", "input"];
138
+ readonly additionalProperties: false;
139
+ };
140
+ readonly result: {
141
+ readonly type: "object";
142
+ readonly properties: {
143
+ readonly raw: {
144
+ readonly type: "object";
145
+ };
146
+ readonly entity: {
147
+ readonly type: "object";
148
+ };
149
+ readonly documentID: {
150
+ readonly type: "string";
151
+ };
152
+ readonly deletedDocumentID: {
153
+ readonly type: "string";
154
+ };
155
+ };
156
+ readonly additionalProperties: false;
157
+ };
158
+ };
159
+ readonly 'connector/push': {
160
+ readonly type: "request";
161
+ readonly param: {
162
+ readonly type: "object";
163
+ readonly properties: {
164
+ readonly connector: {
165
+ readonly type: "string";
166
+ };
167
+ readonly batch: {
168
+ readonly type: "object";
169
+ readonly properties: {
170
+ readonly entities: {
171
+ readonly type: "array";
172
+ readonly items: {
173
+ readonly type: "object";
174
+ };
175
+ };
176
+ readonly deleted: {
177
+ readonly type: "array";
178
+ readonly items: {
179
+ readonly type: "object";
180
+ readonly properties: {
181
+ readonly sourceService: {
182
+ readonly type: "string";
183
+ };
184
+ readonly sourceID: {
185
+ readonly type: "string";
186
+ };
187
+ };
188
+ readonly required: readonly ["sourceService", "sourceID"];
189
+ };
190
+ };
191
+ readonly checkpoint: {};
192
+ readonly hasMore: {
193
+ readonly type: "boolean";
194
+ };
195
+ };
196
+ readonly required: readonly ["entities", "hasMore"];
197
+ readonly additionalProperties: false;
198
+ };
199
+ readonly checkpoint: {};
200
+ };
201
+ readonly required: readonly ["connector", "batch"];
202
+ readonly additionalProperties: false;
203
+ };
204
+ readonly result: {
205
+ readonly type: "object";
206
+ readonly properties: {
207
+ readonly processed: {
208
+ readonly type: "number";
209
+ };
210
+ readonly failed: {
211
+ readonly type: "number";
212
+ };
213
+ readonly documentIDs: {
214
+ readonly type: "array";
215
+ readonly items: {
216
+ readonly type: "string";
217
+ };
218
+ };
219
+ };
220
+ readonly required: readonly ["processed", "failed", "documentIDs"];
221
+ readonly additionalProperties: false;
222
+ };
223
+ };
224
+ readonly 'connector/push-complete': {
225
+ readonly type: "request";
226
+ readonly param: {
227
+ readonly type: "object";
228
+ readonly properties: {
229
+ readonly connector: {
230
+ readonly type: "string";
231
+ };
232
+ readonly checkpoint: {};
233
+ };
234
+ readonly required: readonly ["connector", "checkpoint"];
235
+ readonly additionalProperties: false;
236
+ };
237
+ readonly result: {
238
+ readonly type: "object";
239
+ readonly properties: {
240
+ readonly acknowledged: {
241
+ readonly type: "boolean";
242
+ };
243
+ };
244
+ readonly required: readonly ["acknowledged"];
245
+ readonly additionalProperties: false;
246
+ };
247
+ };
248
+ readonly 'connector/auth-url': {
249
+ readonly type: "request";
250
+ readonly param: {
251
+ readonly type: "object";
252
+ readonly properties: {
253
+ readonly provider: {
254
+ readonly type: "string";
255
+ };
256
+ readonly redirectURL: {
257
+ readonly type: "string";
258
+ };
259
+ readonly connectors: {
260
+ readonly type: "array";
261
+ readonly items: {
262
+ readonly type: "string";
263
+ };
264
+ };
265
+ };
266
+ readonly required: readonly ["provider", "redirectURL"];
267
+ readonly additionalProperties: false;
268
+ };
269
+ readonly result: {
270
+ readonly type: "object";
271
+ readonly properties: {
272
+ readonly url: {
273
+ readonly type: "string";
274
+ };
275
+ readonly state: {
276
+ readonly type: "string";
277
+ };
278
+ };
279
+ readonly required: readonly ["url", "state"];
280
+ readonly additionalProperties: false;
281
+ };
282
+ };
283
+ readonly 'connector/auth-complete': {
284
+ readonly type: "request";
285
+ readonly param: {
286
+ readonly type: "object";
287
+ readonly properties: {
288
+ readonly provider: {
289
+ readonly type: "string";
290
+ };
291
+ readonly code: {
292
+ readonly type: "string";
293
+ };
294
+ readonly state: {
295
+ readonly type: "string";
296
+ };
297
+ };
298
+ readonly required: readonly ["provider", "code", "state"];
299
+ readonly additionalProperties: false;
300
+ };
301
+ readonly result: {
302
+ readonly type: "object";
303
+ readonly properties: {
304
+ readonly providerName: {
305
+ readonly type: "string";
306
+ };
307
+ readonly accountLabel: {
308
+ readonly type: "string";
309
+ };
310
+ readonly scopes: {
311
+ readonly type: "array";
312
+ readonly items: {
313
+ readonly type: "string";
314
+ };
315
+ };
316
+ };
317
+ readonly required: readonly ["providerName", "scopes"];
318
+ readonly additionalProperties: false;
319
+ };
320
+ };
321
+ };
322
+ export type ConnectorProtocol = typeof connectorProtocol;
323
+ export type ConnectorSyncParams = FromSchema<ConnectorProtocol['connector/sync']['param']>;
324
+ export type ConnectorSyncReceive = FromSchema<ConnectorProtocol['connector/sync']['receive']>;
325
+ export type ConnectorSyncResult = FromSchema<ConnectorProtocol['connector/sync']['result']>;
326
+ export type ConnectorStatusParams = FromSchema<ConnectorProtocol['connector/status']['param']>;
327
+ export type ConnectorStatusResult = FromSchema<ConnectorProtocol['connector/status']['result']>;
328
+ export type ConnectorActionParams = FromSchema<ConnectorProtocol['connector/action']['param']>;
329
+ export type ConnectorActionResult = FromSchema<ConnectorProtocol['connector/action']['result']>;
330
+ export type ConnectorPushParams = FromSchema<ConnectorProtocol['connector/push']['param']>;
331
+ export type ConnectorPushResult = FromSchema<ConnectorProtocol['connector/push']['result']>;
332
+ export type ConnectorPushCompleteParams = FromSchema<ConnectorProtocol['connector/push-complete']['param']>;
333
+ export type ConnectorPushCompleteResult = FromSchema<ConnectorProtocol['connector/push-complete']['result']>;
334
+ export type ConnectorAuthURLParams = FromSchema<ConnectorProtocol['connector/auth-url']['param']>;
335
+ export type ConnectorAuthURLResult = FromSchema<ConnectorProtocol['connector/auth-url']['result']>;
336
+ export type ConnectorAuthCompleteParams = FromSchema<ConnectorProtocol['connector/auth-complete']['param']>;
337
+ export type ConnectorAuthCompleteResult = FromSchema<ConnectorProtocol['connector/auth-complete']['result']>;
@@ -0,0 +1 @@
1
+ export const connectorProtocol={"connector/sync":{type:"stream",param:{type:"object",properties:{connector:{type:"string",description:"Connector name"},full:{type:"boolean",description:"Force full sync instead of incremental"}},required:["connector"],additionalProperties:!1},receive:{type:"object",properties:{type:{type:"string",enum:["progress","entity","error"]},connectorName:{type:"string"},phase:{type:"string",enum:["initial","incremental"]},entitiesProcessed:{type:"number"},entitiesFailed:{type:"number"},currentBatch:{type:"number"},checkpoint:{},entity:{type:"object"},documentID:{type:"string"},error:{type:"string"}},additionalProperties:!1},result:{type:"object",properties:{success:{type:"boolean"},totalProcessed:{type:"number"},totalFailed:{type:"number"},duration:{type:"number"}},required:["success","totalProcessed","totalFailed","duration"],additionalProperties:!1}},"connector/status":{type:"request",param:{type:"object",properties:{connector:{type:"string",description:"Optional connector name to filter"}},additionalProperties:!1},result:{type:"object",properties:{connectors:{type:"array",items:{type:"object",properties:{name:{type:"string"},lastSyncedAt:{type:"string"},entityCount:{type:"number"},status:{type:"string",enum:["active","inactive","syncing","error"]},error:{type:"string"}},required:["name","status"],additionalProperties:!1}}},required:["connectors"],additionalProperties:!1}},"connector/action":{type:"request",param:{type:"object",properties:{connector:{type:"string"},action:{type:"string"},input:{type:"object"}},required:["connector","action","input"],additionalProperties:!1},result:{type:"object",properties:{raw:{type:"object"},entity:{type:"object"},documentID:{type:"string"},deletedDocumentID:{type:"string"}},additionalProperties:!1}},"connector/push":{type:"request",param:{type:"object",properties:{connector:{type:"string"},batch:{type:"object",properties:{entities:{type:"array",items:{type:"object"}},deleted:{type:"array",items:{type:"object",properties:{sourceService:{type:"string"},sourceID:{type:"string"}},required:["sourceService","sourceID"]}},checkpoint:{},hasMore:{type:"boolean"}},required:["entities","hasMore"],additionalProperties:!1},checkpoint:{}},required:["connector","batch"],additionalProperties:!1},result:{type:"object",properties:{processed:{type:"number"},failed:{type:"number"},documentIDs:{type:"array",items:{type:"string"}}},required:["processed","failed","documentIDs"],additionalProperties:!1}},"connector/push-complete":{type:"request",param:{type:"object",properties:{connector:{type:"string"},checkpoint:{}},required:["connector","checkpoint"],additionalProperties:!1},result:{type:"object",properties:{acknowledged:{type:"boolean"}},required:["acknowledged"],additionalProperties:!1}},"connector/auth-url":{type:"request",param:{type:"object",properties:{provider:{type:"string"},redirectURL:{type:"string"},connectors:{type:"array",items:{type:"string"}}},required:["provider","redirectURL"],additionalProperties:!1},result:{type:"object",properties:{url:{type:"string"},state:{type:"string"}},required:["url","state"],additionalProperties:!1}},"connector/auth-complete":{type:"request",param:{type:"object",properties:{provider:{type:"string"},code:{type:"string"},state:{type:"string"}},required:["provider","code","state"],additionalProperties:!1},result:{type:"object",properties:{providerName:{type:"string"},accountLabel:{type:"string"},scopes:{type:"array",items:{type:"string"}}},required:["providerName","scopes"],additionalProperties:!1}}};
@@ -0,0 +1,10 @@
1
+ import type { ConnectorDefinition } from './types.js';
2
+ export declare class ConnectorRegistry {
3
+ #private;
4
+ register(connector: ConnectorDefinition): void;
5
+ unregister(name: string): void;
6
+ get(name: string): ConnectorDefinition | undefined;
7
+ has(name: string): boolean;
8
+ list(): Array<string>;
9
+ getAll(): Array<ConnectorDefinition>;
10
+ }
@@ -0,0 +1 @@
1
+ export class ConnectorRegistry{#e=new Map;register(e){if(this.#e.has(e.name))throw Error(`Connector "${e.name}" is already registered`);this.#e.set(e.name,e)}unregister(e){this.#e.delete(e)}get(e){return this.#e.get(e)}has(e){return this.#e.has(e)}list(){return Array.from(this.#e.keys())}getAll(){return Array.from(this.#e.values())}}
@@ -0,0 +1,4 @@
1
+ import type { SyncBoundary } from '../types.js';
2
+ export declare function parseDuration(duration: string): number | undefined;
3
+ export declare function computeEffectiveBoundary(serverBoundary: SyncBoundary, userBoundary: SyncBoundary | undefined): SyncBoundary;
4
+ export declare function isWithinBoundary(date: Date, boundary: SyncBoundary): boolean;
@@ -0,0 +1 @@
1
+ let e=/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;export function parseDuration(n){let i=e.exec(n);if(!i)return;let t=Number.parseInt(i[1]??"0",10),l=Number.parseInt(i[2]??"0",10),a=Number.parseInt(i[3]??"0",10),u=Number.parseInt(i[4]??"0",10);if(0!==t||0!==l||0!==a||0!==u)return((24*t+l)*60+a)*6e4+1e3*u}export function computeEffectiveBoundary(e,n){if(null==n)return e;let i={};if(null!=e.maxAge||null!=n.maxAge){let t=e.maxAge?parseDuration(e.maxAge):void 0,l=n.maxAge?parseDuration(n.maxAge):void 0;null!=t&&null!=l?i.maxAge=t<=l?e.maxAge:n.maxAge:i.maxAge=e.maxAge??n.maxAge}if(null!=e.since||null!=n.since){let t=e.since?new Date(e.since):void 0,l=n.since?new Date(n.since):void 0;null!=t&&null!=l?i.since=t>=l?e.since:n.since:i.since=e.since??n.since}if(null!=e.maxEntities||null!=n.maxEntities){let t=e.maxEntities,l=n.maxEntities;null!=t&&null!=l?i.maxEntities=Math.min(t,l):i.maxEntities=e.maxEntities??n.maxEntities}return i}export function isWithinBoundary(e,n){let i=Date.now();if(null!=n.maxAge){let t=parseDuration(n.maxAge);if(null!=t&&e.getTime()<i-t)return!1}if(null!=n.since){let i=new Date(n.since);if(e.getTime()<i.getTime())return!1}return!0}
package/lib/types.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ import type { ClusterModel } from '@kubun/protocol';
2
+ import type { JSONSchema } from 'json-schema-typed';
3
+ export type SyncBoundary = {
4
+ maxAge?: string;
5
+ since?: string;
6
+ maxEntities?: number;
7
+ };
8
+ export type UserSyncPreferences = {
9
+ connectorName: string;
10
+ boundary?: SyncBoundary;
11
+ excludeChannels?: Array<string>;
12
+ excludeLabels?: Array<string>;
13
+ };
14
+ export type EntityRecord = Record<string, unknown> & {
15
+ sourceService: string;
16
+ sourceID: string;
17
+ };
18
+ export type EntityBatch = {
19
+ entities: Array<EntityRecord>;
20
+ deleted?: Array<{
21
+ sourceService: string;
22
+ sourceID: string;
23
+ }>;
24
+ checkpoint?: unknown;
25
+ hasMore: boolean;
26
+ };
27
+ export type FetchParams = {
28
+ boundary: SyncBoundary;
29
+ checkpoint?: unknown;
30
+ signal?: AbortSignal;
31
+ };
32
+ export type FetchChangesParams = FetchParams & {
33
+ lastSyncedAt?: Date;
34
+ };
35
+ export type DataProvider = {
36
+ fetchAll(params: FetchParams): AsyncIterable<EntityBatch>;
37
+ fetchChanges(params: FetchChangesParams): AsyncIterable<EntityBatch>;
38
+ supportedStrategies: Array<'polling' | 'on-demand'>;
39
+ };
40
+ export type WebhookParams = {
41
+ callbackURL: string;
42
+ secret?: string;
43
+ };
44
+ export type WebhookRegistration = {
45
+ id: string;
46
+ expiresAt?: Date;
47
+ };
48
+ export type ServerDataProvider = DataProvider & {
49
+ supportedStrategies: Array<'polling' | 'on-demand' | 'webhook'>;
50
+ registerWebhook?(params: WebhookParams): Promise<WebhookRegistration>;
51
+ unregisterWebhook?(registrationID: string): Promise<void>;
52
+ };
53
+ export type ClientDataProvider = DataProvider;
54
+ export type DevicePermission = {
55
+ platform: 'ios' | 'android' | 'web';
56
+ permission: string;
57
+ usage: string;
58
+ };
59
+ export type OAuthConnectorAuth = {
60
+ provider: string;
61
+ scopes: Array<string>;
62
+ };
63
+ export type DeviceConnectorAuth = {
64
+ provider: 'device';
65
+ permissions: Array<DevicePermission>;
66
+ };
67
+ export type ConnectorAuth = OAuthConnectorAuth | DeviceConnectorAuth;
68
+ export type ActionDefinition = {
69
+ name: string;
70
+ description: string;
71
+ input: JSONSchema.Object;
72
+ output?: JSONSchema.Object;
73
+ entityModel?: string;
74
+ requiresCredential: boolean;
75
+ };
76
+ export type ActionResult = {
77
+ raw?: Record<string, unknown>;
78
+ entity?: EntityRecord;
79
+ documentID?: string;
80
+ deletedDocumentID?: string;
81
+ };
82
+ export type ServerProviderFactory = (config: {
83
+ credential: unknown;
84
+ boundary: SyncBoundary;
85
+ }) => ServerDataProvider;
86
+ export type ClientProviderFactory = (config: {
87
+ boundary: SyncBoundary;
88
+ }) => ClientDataProvider;
89
+ export type ConnectorDefinition = {
90
+ name: string;
91
+ version: string;
92
+ cluster: ClusterModel;
93
+ syncStrategies: Array<'polling' | 'webhook' | 'on-demand'>;
94
+ actions?: Array<ActionDefinition>;
95
+ serverProvider?: ServerProviderFactory;
96
+ clientProvider?: ClientProviderFactory;
97
+ graphql?: {
98
+ mutations?: boolean;
99
+ };
100
+ auth: ConnectorAuth;
101
+ };
102
+ export type SyncProgressEvent = {
103
+ type: 'sync:progress';
104
+ connectorName: string;
105
+ phase: 'initial' | 'incremental';
106
+ entitiesProcessed: number;
107
+ entitiesFailed: number;
108
+ currentBatch: number;
109
+ checkpoint: unknown;
110
+ };
111
+ export type SyncCompleteEvent = {
112
+ type: 'sync:complete';
113
+ connectorName: string;
114
+ totalProcessed: number;
115
+ totalFailed: number;
116
+ duration: number;
117
+ };
118
+ export type SyncErrorEvent = {
119
+ type: 'sync:error';
120
+ connectorName: string;
121
+ error: string;
122
+ };
123
+ export type SyncEvent = SyncProgressEvent | SyncCompleteEvent | SyncErrorEvent;
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export{};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@kubun/connector",
3
+ "version": "0.5.0",
4
+ "license": "see LICENSE.md",
5
+ "keywords": [],
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "types": "lib/index.d.ts",
9
+ "exports": {
10
+ ".": "./lib/index.js"
11
+ },
12
+ "files": [
13
+ "lib/*",
14
+ "LICENSE.md"
15
+ ],
16
+ "sideEffects": false,
17
+ "dependencies": {
18
+ "@enkaku/event": "^0.13.0",
19
+ "@enkaku/protocol": "^0.13.0",
20
+ "@enkaku/schema": "^0.13.0",
21
+ "hono": "^4.11.8",
22
+ "json-schema-typed": "^8.0.2",
23
+ "@kubun/db": "^0.5.0",
24
+ "@kubun/logger": "^0.5.0",
25
+ "@kubun/models": "^0.5.0",
26
+ "@kubun/scalars": "^0.5.0",
27
+ "@kubun/protocol": "^0.5.1"
28
+ },
29
+ "devDependencies": {},
30
+ "scripts": {
31
+ "build:clean": "del lib",
32
+ "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
33
+ "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
34
+ "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
35
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
36
+ "test:types": "tsc --noEmit",
37
+ "test:unit": "vitest run",
38
+ "test": "pnpm run test:types && pnpm run test:unit"
39
+ }
40
+ }