@context-use/open-sync 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +44 -0
  3. package/src/api.ts +53 -0
  4. package/src/build.ts +6 -0
  5. package/src/connector/client.ts +156 -0
  6. package/src/connector/encryption-key.ts +23 -0
  7. package/src/connector/management.ts +76 -0
  8. package/src/connector/response.ts +35 -0
  9. package/src/db/client.ts +24 -0
  10. package/src/db/providers.sql +15 -0
  11. package/src/db/providers.sql.d.ts +2 -0
  12. package/src/db/providers.ts +24 -0
  13. package/src/db/schema.sql +45 -0
  14. package/src/db/schema.sql.d.ts +2 -0
  15. package/src/execution/diagnostics.ts +18 -0
  16. package/src/execution/provider.ts +37 -0
  17. package/src/execution/worker.ts +90 -0
  18. package/src/http/app.ts +21 -0
  19. package/src/http/controller.ts +95 -0
  20. package/src/http/index.ts +20 -0
  21. package/src/http/providers.ts +94 -0
  22. package/src/models/definition.ts +63 -0
  23. package/src/models/delivery-result.ts +22 -0
  24. package/src/models/delivery.ts +73 -0
  25. package/src/models/error.ts +11 -0
  26. package/src/models/identity.ts +11 -0
  27. package/src/models/installation.ts +37 -0
  28. package/src/models/json.ts +82 -0
  29. package/src/models/limits.ts +42 -0
  30. package/src/models/page.ts +69 -0
  31. package/src/models/provider-values.ts +42 -0
  32. package/src/models/providers.ts +34 -0
  33. package/src/models/registry.ts +74 -0
  34. package/src/models/validation.ts +24 -0
  35. package/src/open-sync.ts +182 -0
  36. package/src/repositories/acquisition/contract.ts +17 -0
  37. package/src/repositories/acquisition/lease.ts +118 -0
  38. package/src/repositories/acquisition/outbox.ts +52 -0
  39. package/src/repositories/acquisition/records.ts +37 -0
  40. package/src/repositories/acquisition/sqlite.ts +60 -0
  41. package/src/repositories/catalog/contract.ts +23 -0
  42. package/src/repositories/catalog/sqlite.ts +166 -0
  43. package/src/repositories/delivery/contract.ts +23 -0
  44. package/src/repositories/delivery/sqlite.ts +111 -0
  45. package/src/repositories/providers/contract.ts +15 -0
  46. package/src/repositories/providers/sqlite.ts +79 -0
  47. package/src/repositories/queue-usage.ts +21 -0
  48. package/src/repositories/rows.ts +50 -0
  49. package/src/runtime.ts +90 -0
  50. package/src/services/acquisition.ts +100 -0
  51. package/src/services/delivery.ts +53 -0
  52. package/src/services/management.ts +124 -0
  53. package/src/services/providers/service.ts +190 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@context-use/open-sync",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/open-sync.ts",
7
+ "./definition": "./src/models/definition.ts",
8
+ "./delivery": "./src/models/delivery.ts",
9
+ "./json": "./src/models/json.ts",
10
+ "./http": "./src/http/index.ts",
11
+ "./engine": "./src/runtime.ts",
12
+ "./build": "./src/build.ts"
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "check:types": "tsc --noEmit",
19
+ "test": "bun test test",
20
+ "prepack": "cp ../../LICENSE ./LICENSE",
21
+ "postpack": "rm ./LICENSE"
22
+ },
23
+ "dependencies": {
24
+ "@cfworker/json-schema": "4.1.1",
25
+ "elysia": "^1.4.30",
26
+ "@oomol-lab/open-connector": "1.6.0"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "engines": {
32
+ "bun": ">=1.4.0"
33
+ },
34
+ "description": "Headless sync runtime for Bun with provider authorization, durable checkpoints, and record delivery.",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/massimoalbarello/open-sync.git",
39
+ "directory": "packages/sync"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
package/src/api.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { SyncManagement } from './services/management';
2
+ import type { ProviderService } from './services/providers/service';
3
+
4
+ /** Deliberate public selections: internal service methods are not automatically host APIs. */
5
+ export type SyncApi = Pick<
6
+ SyncManagement,
7
+ | 'definitions'
8
+ | 'destinationTypes'
9
+ | 'runs'
10
+ | 'destinations'
11
+ | 'createDestination'
12
+ | 'createInstallation'
13
+ | 'installations'
14
+ | 'installation'
15
+ | 'setEnabled'
16
+ | 'queueRun'
17
+ | 'status'
18
+ | 'deliveries'
19
+ | 'retryDelivery'
20
+ >;
21
+ export type ProviderApi = Pick<
22
+ ProviderService,
23
+ 'connections' | 'connection' | 'catalog' | 'status' | 'configure' | 'start' | 'credentials'
24
+ >;
25
+
26
+ export function syncApi(service: SyncManagement): SyncApi {
27
+ return {
28
+ definitions: service.definitions.bind(service),
29
+ destinations: service.destinations.bind(service),
30
+ destinationTypes: service.destinationTypes.bind(service),
31
+ runs: service.runs.bind(service),
32
+ createDestination: service.createDestination.bind(service),
33
+ createInstallation: service.createInstallation.bind(service),
34
+ installations: service.installations.bind(service),
35
+ installation: service.installation.bind(service),
36
+ setEnabled: service.setEnabled.bind(service),
37
+ queueRun: service.queueRun.bind(service),
38
+ status: service.status.bind(service),
39
+ deliveries: service.deliveries.bind(service),
40
+ retryDelivery: service.retryDelivery.bind(service),
41
+ };
42
+ }
43
+ export function providerApi(service: ProviderService): ProviderApi {
44
+ return {
45
+ connections: service.connections.bind(service),
46
+ connection: service.connection.bind(service),
47
+ catalog: service.catalog.bind(service),
48
+ status: service.status.bind(service),
49
+ configure: service.configure.bind(service),
50
+ start: service.start.bind(service),
51
+ credentials: service.credentials.bind(service),
52
+ };
53
+ }
package/src/build.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { getConnectorAssetDirectory } from '@oomol-lab/open-connector';
2
+
3
+ /** Options needed when a host embeds Open Sync in a Bun executable. */
4
+ export function getOpenSyncBuildOptions() {
5
+ return { assets: [getConnectorAssetDirectory()], external: ['proxy-agent'] };
6
+ }
@@ -0,0 +1,156 @@
1
+ import type { ProviderGateway } from '../execution/provider';
2
+ import type { ConnectionRef, ProviderOperations, ProviderRequirements } from '../models/definition';
3
+ import { fail } from '../models/error';
4
+ import type { Scope } from '../models/identity';
5
+ import { canonicalJson, type JsonObject, type JsonValue } from '../models/json';
6
+ import { providerResponse } from './response';
7
+
8
+ interface RequestInput {
9
+ path: string;
10
+ admin?: boolean;
11
+ body?: JsonObject;
12
+ alias?: string;
13
+ signal: AbortSignal;
14
+ }
15
+ interface ConnectorClientOptions {
16
+ /** Borrowed embedded Connector transport; the Open Sync facade owns its lifetime. */
17
+ fetch(request: Request): Promise<Response>;
18
+ baseUrl: string;
19
+ adminToken: string;
20
+ runtimeToken: string;
21
+ /** The facade checks ownership before any privileged metadata read. */
22
+ authorizeConnection(input: Scope & { connection: ConnectionRef }): Promise<boolean>;
23
+ }
24
+
25
+ /** Public APIs only. Connector currently exposes no atomic credential-generation commit guard. */
26
+ export function createConnectorClient(options: ConnectorClientOptions): ProviderGateway {
27
+ if (!options.adminToken || !options.runtimeToken) {
28
+ fail('connector_credentials_required');
29
+ }
30
+ const base = options.baseUrl.replace(/\/$/, '');
31
+ async function request(input: RequestInput): Promise<JsonValue> {
32
+ input.signal.throwIfAborted();
33
+ const headers = new Headers({
34
+ authorization: `Bearer ${input.admin ? options.adminToken : options.runtimeToken}`,
35
+ });
36
+ if (input.alias) {
37
+ headers.set('x-oo-connector-alias', input.alias);
38
+ }
39
+ if (input.body) {
40
+ headers.set('content-type', 'application/json');
41
+ }
42
+ const response = await options.fetch(
43
+ new Request(`${base}${input.path}`, {
44
+ method: input.body ? 'POST' : 'GET',
45
+ headers,
46
+ signal: input.signal,
47
+ body: input.body ? JSON.stringify(input.body) : undefined,
48
+ }),
49
+ );
50
+ const result = (await response.json()) as { success?: boolean; data?: JsonValue };
51
+ if (!response.ok || result.success !== true || result.data === undefined) {
52
+ fail('connector_request_failed');
53
+ }
54
+ input.signal.throwIfAborted();
55
+ return canonicalJson(result.data).value;
56
+ }
57
+ return {
58
+ async bind(input) {
59
+ if (
60
+ !(await options.authorizeConnection({
61
+ actorId: input.actorId,
62
+ ownerId: input.ownerId,
63
+ connection: input.connection,
64
+ }))
65
+ ) {
66
+ fail('not_found');
67
+ }
68
+ if (input.connection.service !== input.requirements.service) {
69
+ fail('connection_mismatch');
70
+ }
71
+ const metadata = (await request({
72
+ path: `/v1/connections/by-id/${encodeURIComponent(input.connection.id)}`,
73
+ admin: true,
74
+ signal: input.signal,
75
+ })) as JsonObject;
76
+ checkConnection({ metadata, connection: input.connection, requirements: input.requirements });
77
+ return operations({
78
+ request,
79
+ requirements: input.requirements,
80
+ alias: metadata.alias as string,
81
+ signal: input.signal,
82
+ });
83
+ },
84
+ };
85
+ }
86
+ function checkConnection(input: {
87
+ metadata: JsonObject;
88
+ connection: ConnectionRef;
89
+ requirements: ProviderRequirements;
90
+ }): void {
91
+ const { metadata, connection, requirements } = input;
92
+ if (
93
+ metadata.id !== connection.id ||
94
+ metadata.service !== requirements.service ||
95
+ metadata.status !== 'active' ||
96
+ typeof metadata.alias !== 'string' ||
97
+ !metadata.alias
98
+ ) {
99
+ fail('connection_unavailable');
100
+ }
101
+ }
102
+ function operations(input: {
103
+ request(input: RequestInput): Promise<JsonValue>;
104
+ requirements: ProviderRequirements;
105
+ alias: string;
106
+ signal: AbortSignal;
107
+ }): ProviderOperations {
108
+ const { request, requirements, alias, signal } = input;
109
+ return {
110
+ async action(operation) {
111
+ if (!requirements.actions.includes(operation.id)) {
112
+ fail('operation_denied');
113
+ }
114
+ const path = `/v1/actions/${encodeURIComponent(operation.id)}`;
115
+ const metadata = (await request({ path, signal })) as JsonObject;
116
+ if (metadata.service !== requirements.service) {
117
+ fail('operation_denied');
118
+ }
119
+ return await request({ path, body: { input: operation.input }, alias, signal });
120
+ },
121
+ get(operation) {
122
+ const { path } = operation;
123
+ if (
124
+ !path.startsWith('/') ||
125
+ path.startsWith('//') ||
126
+ /[\\?#]/.test(path) ||
127
+ !requirements.proxyPaths?.includes(path)
128
+ ) {
129
+ fail('operation_denied');
130
+ }
131
+ return request({
132
+ path: `/v1/proxy/${encodeURIComponent(requirements.service)}`,
133
+ body: { endpoint: path, method: 'GET', query: operation.query ?? {} },
134
+ alias,
135
+ signal,
136
+ }).then(providerResponse);
137
+ },
138
+ post(operation) {
139
+ const { path } = operation;
140
+ if (
141
+ !path.startsWith('/') ||
142
+ path.startsWith('//') ||
143
+ /[\\?#]/.test(path) ||
144
+ !requirements.proxyPostPaths?.includes(path)
145
+ ) {
146
+ fail('operation_denied');
147
+ }
148
+ return request({
149
+ path: `/v1/proxy/${encodeURIComponent(requirements.service)}`,
150
+ body: { endpoint: path, method: 'POST', body: operation.body },
151
+ alias,
152
+ signal,
153
+ }).then(providerResponse);
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,23 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ /** Independent from login secrets; retain this key alongside the opaque Connector data. */
6
+ export async function loadProviderKey(dataFolder: string): Promise<string> {
7
+ await mkdir(dataFolder, { recursive: true });
8
+ const path = join(dataFolder, '.connector-key');
9
+ const bytes = 32;
10
+ const mode = 0o600;
11
+ try {
12
+ await writeFile(path, randomBytes(bytes).toString('hex'), { flag: 'wx', mode });
13
+ } catch (error) {
14
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) {
15
+ throw error;
16
+ }
17
+ }
18
+ const value = await readFile(path, 'utf8');
19
+ if (!/^[a-f0-9]{64}$/.test(value)) {
20
+ throw new Error('Invalid provider encryption key.');
21
+ }
22
+ return value;
23
+ }
@@ -0,0 +1,76 @@
1
+ import { SyncError } from '../models/error';
2
+ import type { ConnectorManagement, ProviderCatalogEntry } from '../models/providers';
3
+
4
+ /** Only this internal adapter uses administrator credentials. No raw Connector routes are exposed to users. */
5
+ export function connectorManagement(input: {
6
+ fetch(request: Request): Promise<Response>;
7
+ baseUrl: string;
8
+ adminToken: string;
9
+ runtimeToken: string;
10
+ signal: AbortSignal;
11
+ }): ConnectorManagement {
12
+ const timeoutMs = 30_000;
13
+ async function requestData({
14
+ request,
15
+ token,
16
+ }: {
17
+ request: { path: string; method?: 'POST' | 'PUT'; body?: Record<string, unknown> };
18
+ token: string;
19
+ }): Promise<unknown> {
20
+ const response = await input.fetch(
21
+ new Request(`${input.baseUrl}${request.path}`, {
22
+ method: request.method ?? 'GET',
23
+ headers: {
24
+ authorization: `Bearer ${token}`,
25
+ 'content-type': 'application/json',
26
+ },
27
+ body: request.body ? JSON.stringify(request.body) : undefined,
28
+ signal: AbortSignal.any([input.signal, AbortSignal.timeout(timeoutMs)]),
29
+ }),
30
+ );
31
+ const result = (await response.json()) as Record<string, unknown>;
32
+ input.signal.throwIfAborted();
33
+ // The documented OAuth configuration API returns a bare summary; /v1 uses an envelope.
34
+ const versioned = request.path.startsWith('/v1/');
35
+ const data = versioned ? result.data : result;
36
+ if (
37
+ !response.ok ||
38
+ (versioned && result.success !== true) ||
39
+ !data ||
40
+ typeof data !== 'object'
41
+ ) {
42
+ // Do not echo provider errors or request bodies, which can contain credentials.
43
+ throw new SyncError({
44
+ code: 'provider_request_failed',
45
+ message:
46
+ 'Provider could not complete this request. Check provider configuration and authorization.',
47
+ });
48
+ }
49
+ return data;
50
+ }
51
+ return {
52
+ async catalog() {
53
+ const data = await requestData({
54
+ request: { path: '/v1/providers' },
55
+ token: input.runtimeToken,
56
+ });
57
+ if (!Array.isArray(data)) {
58
+ throw new SyncError({
59
+ code: 'provider_request_failed',
60
+ message: 'Invalid provider catalog.',
61
+ });
62
+ }
63
+ return data as ProviderCatalogEntry[];
64
+ },
65
+ async call(request) {
66
+ const data = await requestData({ request, token: input.adminToken });
67
+ if (Array.isArray(data)) {
68
+ throw new SyncError({
69
+ code: 'provider_request_failed',
70
+ message: 'Invalid provider response.',
71
+ });
72
+ }
73
+ return data as Record<string, unknown>;
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,35 @@
1
+ import type { ProviderResponse } from '../models/definition';
2
+ import { fail } from '../models/error';
3
+ import type { JsonValue } from '../models/json';
4
+ import { validate } from '../models/validation';
5
+
6
+ /** Connector's wire envelope ends here; definitions only consume the Open Sync contract. */
7
+ export function providerResponse(value: unknown): ProviderResponse {
8
+ let response: {
9
+ status: number;
10
+ headers: Record<string, string>;
11
+ data: JsonValue;
12
+ bodyEncoding?: unknown;
13
+ };
14
+ try {
15
+ response = validate({
16
+ value,
17
+ schema: {
18
+ type: 'object',
19
+ required: ['status', 'headers', 'data'],
20
+ properties: {
21
+ status: { type: 'integer', minimum: 100, maximum: 599 },
22
+ headers: { type: 'object', additionalProperties: { type: 'string' } },
23
+ data: {},
24
+ },
25
+ },
26
+ }) as unknown as typeof response;
27
+ } catch {
28
+ return fail('invalid_provider_response');
29
+ }
30
+ // Assets are deferred. Never pass an encoded binary body off as provider text.
31
+ if (response.bodyEncoding !== undefined) {
32
+ fail('unsupported_provider_response');
33
+ }
34
+ return { status: response.status, headers: response.headers, body: response.data };
35
+ }
@@ -0,0 +1,24 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { fail } from '../models/error';
3
+ import schema from './schema.sql' with { type: 'text' };
4
+
5
+ export function openDatabase(path: string): Database {
6
+ const db = new Database(path, { create: true, strict: true });
7
+ try {
8
+ db.exec('PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;');
9
+ db.transaction(() => {
10
+ const { user_version: version } = db
11
+ .query<{ user_version: number }, []>('PRAGMA user_version')
12
+ .get()!;
13
+ if (version === 0) {
14
+ db.exec(schema);
15
+ } else if (version !== 1) {
16
+ fail('schema_version');
17
+ }
18
+ }).immediate();
19
+ return db;
20
+ } catch (error) {
21
+ db.close();
22
+ throw error;
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ CREATE TABLE provider_authorizations (
2
+ owner_id TEXT NOT NULL PRIMARY KEY,
3
+ id TEXT NOT NULL UNIQUE,
4
+ request_id TEXT NOT NULL UNIQUE,
5
+ service TEXT NOT NULL
6
+ );
7
+ CREATE TABLE provider_connections (
8
+ owner_id TEXT NOT NULL,
9
+ id TEXT NOT NULL,
10
+ connector_id TEXT NOT NULL UNIQUE,
11
+ account TEXT NOT NULL,
12
+ service TEXT NOT NULL,
13
+ PRIMARY KEY (owner_id, id)
14
+ );
15
+ PRAGMA user_version=1;
@@ -0,0 +1,2 @@
1
+ declare const sql: string;
2
+ export default sql;
@@ -0,0 +1,24 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { fail } from '../models/error';
3
+ import schema from './providers.sql' with { type: 'text' };
4
+
5
+ export function openProviderDatabase(path: string) {
6
+ const db = new Database(path, { create: true, strict: true });
7
+ try {
8
+ db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000');
9
+ db.transaction(() => {
10
+ const version = db
11
+ .query<{ user_version: number }, []>('PRAGMA user_version')
12
+ .get()!.user_version;
13
+ if (version === 0) {
14
+ db.exec(schema);
15
+ } else if (version !== 1) {
16
+ fail('provider_schema_version');
17
+ }
18
+ }).immediate();
19
+ return db;
20
+ } catch (error) {
21
+ db.close();
22
+ throw error;
23
+ }
24
+ }
@@ -0,0 +1,45 @@
1
+ CREATE TABLE definitions (
2
+ id TEXT NOT NULL, version TEXT NOT NULL, artifact_id TEXT NOT NULL, manifest TEXT NOT NULL,
3
+ PRIMARY KEY(id, version)
4
+ );
5
+ CREATE TABLE destinations (
6
+ owner_id TEXT NOT NULL, id TEXT NOT NULL, type TEXT NOT NULL, version TEXT NOT NULL, config TEXT NOT NULL,
7
+ PRIMARY KEY(owner_id, id)
8
+ );
9
+ CREATE TABLE installations (
10
+ owner_id TEXT NOT NULL, id TEXT NOT NULL, source_id TEXT NOT NULL,
11
+ definition_id TEXT NOT NULL, definition_version TEXT NOT NULL, artifact_id TEXT NOT NULL,
12
+ connection TEXT, config TEXT NOT NULL, destination_id TEXT NOT NULL,
13
+ enabled INTEGER NOT NULL, binding_epoch INTEGER NOT NULL DEFAULT 1,
14
+ checkpoint TEXT NOT NULL, checkpoint_revision INTEGER NOT NULL DEFAULT 0,
15
+ interval_ms INTEGER NOT NULL, next_due_at INTEGER NOT NULL, status TEXT NOT NULL,
16
+ PRIMARY KEY(owner_id, id), UNIQUE(owner_id, source_id),
17
+ FOREIGN KEY(owner_id, destination_id) REFERENCES destinations(owner_id, id)
18
+ );
19
+ CREATE TABLE runs (
20
+ owner_id TEXT NOT NULL, id TEXT NOT NULL, installation_id TEXT NOT NULL,
21
+ definition_ref TEXT NOT NULL, binding_epoch INTEGER NOT NULL,
22
+ worker_id TEXT NOT NULL, generation INTEGER NOT NULL, expires_at INTEGER NOT NULL,
23
+ checkpoint_revision INTEGER NOT NULL, state TEXT NOT NULL, started_at INTEGER NOT NULL,
24
+ completed_at INTEGER, pages INTEGER NOT NULL DEFAULT 0,
25
+ PRIMARY KEY(owner_id, id), FOREIGN KEY(owner_id, installation_id) REFERENCES installations(owner_id, id)
26
+ );
27
+ CREATE UNIQUE INDEX one_acquisition ON runs((1)) WHERE state='running';
28
+ CREATE TABLE records (
29
+ owner_id TEXT NOT NULL, installation_id TEXT NOT NULL, kind TEXT NOT NULL, id TEXT NOT NULL,
30
+ hash TEXT NOT NULL, revision INTEGER NOT NULL, deleted INTEGER NOT NULL,
31
+ PRIMARY KEY(owner_id, installation_id, kind, id),
32
+ FOREIGN KEY(owner_id, installation_id) REFERENCES installations(owner_id, id)
33
+ );
34
+ CREATE TABLE deliveries (
35
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT, owner_id TEXT NOT NULL, id TEXT NOT NULL,
36
+ installation_id TEXT NOT NULL, destination_id TEXT NOT NULL, body TEXT NOT NULL,
37
+ bytes INTEGER NOT NULL, record_count INTEGER NOT NULL, state TEXT NOT NULL DEFAULT 'pending',
38
+ due_at INTEGER NOT NULL, worker_id TEXT, generation INTEGER NOT NULL DEFAULT 0,
39
+ expires_at INTEGER, attempt INTEGER NOT NULL DEFAULT 0, error_code TEXT,
40
+ UNIQUE(owner_id, id),
41
+ FOREIGN KEY(owner_id, installation_id) REFERENCES installations(owner_id, id),
42
+ FOREIGN KEY(owner_id, destination_id) REFERENCES destinations(owner_id, id)
43
+ );
44
+ CREATE INDEX deliveries_due ON deliveries(state, due_at);
45
+ PRAGMA user_version = 1;
@@ -0,0 +1,2 @@
1
+ declare const sql: string;
2
+ export default sql;
@@ -0,0 +1,18 @@
1
+ import type { JsonObject } from '../models/json';
2
+ export interface SyncEvent {
3
+ code: string;
4
+ ownerId?: string;
5
+ installationId?: string;
6
+ message?: string;
7
+ fields?: JsonObject;
8
+ }
9
+ export type Logger = (event: SyncEvent) => void;
10
+ export function safeLogger(logger?: Logger): Logger {
11
+ return (event) => {
12
+ try {
13
+ logger?.(event);
14
+ } catch {
15
+ /* Diagnostics cannot change persistence. */
16
+ }
17
+ };
18
+ }
@@ -0,0 +1,37 @@
1
+ import type { ConnectionRef, ProviderOperations, ProviderRequirements } from '../models/definition';
2
+ import { fail } from '../models/error';
3
+ import type { Scope } from '../models/identity';
4
+ export interface ProviderGateway {
5
+ bind(
6
+ input: Scope & {
7
+ connection: ConnectionRef;
8
+ requirements: ProviderRequirements;
9
+ signal: AbortSignal;
10
+ },
11
+ ): Promise<ProviderOperations>;
12
+ }
13
+ export const noProvider: ProviderOperations = {
14
+ action: () => Promise.reject(new Error('operation_denied')),
15
+ get: () => Promise.reject(new Error('operation_denied')),
16
+ post: () => Promise.reject(new Error('operation_denied')),
17
+ };
18
+ export async function bindProvider(
19
+ input: Scope & {
20
+ gateway?: ProviderGateway;
21
+ connection?: ConnectionRef;
22
+ requirements?: ProviderRequirements;
23
+ signal: AbortSignal;
24
+ },
25
+ ): Promise<ProviderOperations> {
26
+ if (!input.requirements) {
27
+ return noProvider;
28
+ }
29
+ if (!input.gateway || !input.connection) {
30
+ return fail('connection_required');
31
+ }
32
+ return await input.gateway.bind({
33
+ ...input,
34
+ connection: input.connection,
35
+ requirements: input.requirements,
36
+ });
37
+ }