@synapseone/sdk 0.7.0 → 0.7.1-dev.56
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/README.md +1 -1
- package/dist/_http.d.ts +57 -4
- package/dist/_http.js +269 -22
- package/dist/ai.js +2 -0
- package/dist/base.d.ts +11 -0
- package/dist/base.js +6 -0
- package/dist/client.test.js +18 -0
- package/dist/example.js +3 -1
- package/dist/faststore.js +2 -0
- package/dist/health.d.ts +54 -0
- package/dist/health.js +58 -0
- package/dist/iam.d.ts +103 -3
- package/dist/iam.js +77 -5
- package/dist/index.d.ts +17 -1
- package/dist/index.js +27 -2
- package/dist/inference.d.ts +28 -5
- package/dist/inference.js +55 -8
- package/dist/lds.js +2 -0
- package/dist/notifications.d.ts +70 -0
- package/dist/notifications.js +55 -0
- package/dist/notifications.test.d.ts +1 -0
- package/dist/notifications.test.js +73 -0
- package/dist/queue.js +2 -0
- package/dist/services.test.js +2 -0
- package/dist/storage.d.ts +66 -0
- package/dist/storage.js +98 -0
- package/dist/storage.test.d.ts +1 -0
- package/dist/storage.test.js +139 -0
- package/dist/stream.js +2 -0
- package/dist/transport.test.d.ts +1 -0
- package/dist/transport.test.js +198 -0
- package/dist/workflow.d.ts +6 -0
- package/dist/workflow.js +21 -3
- package/package.json +33 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
// SPDX-FileCopyrightText: 2026 VKU.VIC
|
|
8
|
+
/*
|
|
9
|
+
* Copyright 2026 SynapseOne Contributors
|
|
10
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
* Notifications SDK tests (stub gateway).
|
|
12
|
+
*/
|
|
13
|
+
const node_test_1 = require("node:test");
|
|
14
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
15
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
16
|
+
const index_1 = require("./index");
|
|
17
|
+
let server;
|
|
18
|
+
let base = '';
|
|
19
|
+
let last = {};
|
|
20
|
+
(0, node_test_1.before)(() => new Promise((resolve) => {
|
|
21
|
+
server = node_http_1.default
|
|
22
|
+
.createServer((req, res) => {
|
|
23
|
+
let body = '';
|
|
24
|
+
req.on('data', (c) => (body += c));
|
|
25
|
+
req.on('end', () => {
|
|
26
|
+
last = { method: req.method, path: req.url, body: body ? JSON.parse(body) : null };
|
|
27
|
+
res.setHeader('Content-Type', 'application/json');
|
|
28
|
+
if (req.url === '/v1/notifications' && req.method === 'POST') {
|
|
29
|
+
res.statusCode = 202;
|
|
30
|
+
res.end(JSON.stringify({ notifications: [{ id: 'n1', status: 'queued' }] }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
res.end(JSON.stringify({ id: 'n1', status: 'delivered', unsubscribed: false, channels: [] }));
|
|
34
|
+
});
|
|
35
|
+
})
|
|
36
|
+
.listen(0, '127.0.0.1', () => {
|
|
37
|
+
const a = server.address();
|
|
38
|
+
base = `http://127.0.0.1:${typeof a === 'object' && a ? a.port : 0}`;
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
}));
|
|
42
|
+
(0, node_test_1.after)(() => new Promise((resolve) => server.close(() => resolve())));
|
|
43
|
+
(0, node_test_1.describe)('SynapseNotifications SDK', () => {
|
|
44
|
+
(0, node_test_1.it)('sends a templated notification', async () => {
|
|
45
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base });
|
|
46
|
+
const out = await c.notifications.send({
|
|
47
|
+
channel: 'webhook',
|
|
48
|
+
recipient: { url: 'https://example.com/hook' },
|
|
49
|
+
subject: 'Hi {{name}}',
|
|
50
|
+
body: 'Hello {{name}}',
|
|
51
|
+
data: { name: 'An' },
|
|
52
|
+
});
|
|
53
|
+
strict_1.default.equal(out.notifications[0].id, 'n1');
|
|
54
|
+
strict_1.default.equal(last.path, '/v1/notifications');
|
|
55
|
+
strict_1.default.equal(last.body.subject, 'Hi {{name}}');
|
|
56
|
+
strict_1.default.equal(last.body.recipient.url, 'https://example.com/hook');
|
|
57
|
+
});
|
|
58
|
+
(0, node_test_1.it)('gets, lists, cancels, preferences and unsubscribe', async () => {
|
|
59
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base });
|
|
60
|
+
await c.notifications.get('n1');
|
|
61
|
+
strict_1.default.equal(last.path, '/v1/notifications/n1');
|
|
62
|
+
await c.notifications.list({ status: 'queued', limit: 5 });
|
|
63
|
+
strict_1.default.equal(last.path, '/v1/notifications?status=queued&limit=5');
|
|
64
|
+
await c.notifications.cancel('n1');
|
|
65
|
+
strict_1.default.equal(last.path, '/v1/notifications/n1/cancel');
|
|
66
|
+
await c.notifications.getPreference('user:u1');
|
|
67
|
+
strict_1.default.equal(last.path, '/v1/notifications/preferences/user%3Au1');
|
|
68
|
+
await c.notifications.setPreference('user:u1', { channels: ['email'], unsubscribed: false });
|
|
69
|
+
strict_1.default.deepEqual(last.body, { channels: ['email'], unsubscribed: false });
|
|
70
|
+
await c.notifications.unsubscribe('user:u1');
|
|
71
|
+
strict_1.default.deepEqual(last.body, { recipient: 'user:u1' });
|
|
72
|
+
});
|
|
73
|
+
});
|
package/dist/queue.js
CHANGED
package/dist/services.test.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
// SPDX-FileCopyrightText: 2026 VKU.VIC
|
|
6
8
|
/*
|
|
7
9
|
* Copyright 2026 SynapseOne Contributors
|
|
8
10
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HttpClient, RequestOptions } from './_http';
|
|
2
|
+
export interface StoredObject {
|
|
3
|
+
key: string;
|
|
4
|
+
size: number;
|
|
5
|
+
contentType: string;
|
|
6
|
+
etag: string;
|
|
7
|
+
checksumSha256?: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ObjectList {
|
|
12
|
+
objects: StoredObject[];
|
|
13
|
+
nextCursor: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface PutObjectOptions extends RequestOptions {
|
|
16
|
+
contentType?: string;
|
|
17
|
+
/** Hex sha256 of the body; the server rejects a mismatch. */
|
|
18
|
+
checksumSha256?: string;
|
|
19
|
+
/** Overwrite an existing object (the default is an upsert). */
|
|
20
|
+
overwrite?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class StorageService {
|
|
23
|
+
private readonly http;
|
|
24
|
+
constructor(http: HttpClient);
|
|
25
|
+
/** Upload raw bytes (Uint8Array, Buffer, ArrayBuffer or a string). */
|
|
26
|
+
putObject(key: string, body: Uint8Array | ArrayBuffer | string, opts?: PutObjectOptions): Promise<StoredObject>;
|
|
27
|
+
/** Download an object as bytes (use `text()`/`blob()` on your side). */
|
|
28
|
+
getObject(key: string, opts?: RequestOptions): Promise<Uint8Array>;
|
|
29
|
+
/** HEAD metadata for an object. Returns null when it does not exist. */
|
|
30
|
+
headObject(key: string, opts?: RequestOptions): Promise<StoredObject | null>;
|
|
31
|
+
deleteObject(key: string): Promise<{
|
|
32
|
+
ok: boolean;
|
|
33
|
+
key: string;
|
|
34
|
+
}>;
|
|
35
|
+
listObjects(opts?: {
|
|
36
|
+
prefix?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
cursor?: string;
|
|
39
|
+
}): Promise<ObjectList>;
|
|
40
|
+
/** Presigned URL (S3 backend only; 501 on the filesystem backend). */
|
|
41
|
+
presign(key: string, opts?: {
|
|
42
|
+
method?: 'GET' | 'PUT' | 'DELETE' | 'HEAD';
|
|
43
|
+
expiresIn?: number;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
url: string;
|
|
46
|
+
method: string;
|
|
47
|
+
expiresIn: number;
|
|
48
|
+
}>;
|
|
49
|
+
createMultipart(key: string, contentType?: string): Promise<{
|
|
50
|
+
uploadId: string;
|
|
51
|
+
key: string;
|
|
52
|
+
contentType: string;
|
|
53
|
+
}>;
|
|
54
|
+
uploadPart(uploadId: string, key: string, partNumber: number, body: Uint8Array | string): Promise<{
|
|
55
|
+
partNumber: number;
|
|
56
|
+
etag: string;
|
|
57
|
+
size: number;
|
|
58
|
+
}>;
|
|
59
|
+
completeMultipart(uploadId: string, key: string, parts: Array<{
|
|
60
|
+
partNumber: number;
|
|
61
|
+
etag: string;
|
|
62
|
+
}>): Promise<{
|
|
63
|
+
object: StoredObject;
|
|
64
|
+
}>;
|
|
65
|
+
abortMultipart(uploadId: string, key: string): Promise<unknown>;
|
|
66
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// SPDX-FileCopyrightText: 2026 VKU.VIC
|
|
4
|
+
/*
|
|
5
|
+
* Copyright 2026 SynapseOne Contributors
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* SynapseObjectStorage client: tenant-scoped object/file storage (#24).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.StorageService = void 0;
|
|
11
|
+
const P = '/v1/storage';
|
|
12
|
+
class StorageService {
|
|
13
|
+
http;
|
|
14
|
+
constructor(http) {
|
|
15
|
+
this.http = http;
|
|
16
|
+
}
|
|
17
|
+
/** Upload raw bytes (Uint8Array, Buffer, ArrayBuffer or a string). */
|
|
18
|
+
async putObject(key, body, opts = {}) {
|
|
19
|
+
const bytes = typeof body === 'string' ? new TextEncoder().encode(body) : body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
20
|
+
const res = await this.http.upload(`${P}/objects/${encodeKey(key)}`, bytes, {
|
|
21
|
+
...opts,
|
|
22
|
+
contentType: opts.contentType || 'application/octet-stream',
|
|
23
|
+
headers: { ...(opts.checksumSha256 ? { 'x-object-checksum-sha256': opts.checksumSha256 } : {}), ...(opts.headers || {}) },
|
|
24
|
+
});
|
|
25
|
+
return res.object;
|
|
26
|
+
}
|
|
27
|
+
/** Download an object as bytes (use `text()`/`blob()` on your side). */
|
|
28
|
+
getObject(key, opts = {}) {
|
|
29
|
+
return this.http.reqBinary(`${P}/objects/${encodeKey(key)}`, undefined, opts);
|
|
30
|
+
}
|
|
31
|
+
/** HEAD metadata for an object. Returns null when it does not exist. */
|
|
32
|
+
async headObject(key, opts = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const h = await this.http.head(`${P}/objects/${encodeKey(key)}`, opts);
|
|
35
|
+
return {
|
|
36
|
+
key: h['x-object-key'] || key,
|
|
37
|
+
size: Number(h['content-length'] || 0),
|
|
38
|
+
contentType: h['content-type'] || 'application/octet-stream',
|
|
39
|
+
etag: (h['etag'] || '').replace(/"/g, ''),
|
|
40
|
+
checksumSha256: h['x-object-checksum-sha256'],
|
|
41
|
+
createdAt: h['x-object-created-at'] || '',
|
|
42
|
+
updatedAt: h['x-object-updated-at'] || '',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e?.status === 404)
|
|
47
|
+
return null;
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
deleteObject(key) {
|
|
52
|
+
return this.http.req(`${P}/objects/${encodeKey(key)}`, { method: 'DELETE' });
|
|
53
|
+
}
|
|
54
|
+
listObjects(opts = {}) {
|
|
55
|
+
const qs = new URLSearchParams();
|
|
56
|
+
if (opts.prefix)
|
|
57
|
+
qs.set('prefix', opts.prefix);
|
|
58
|
+
if (opts.limit)
|
|
59
|
+
qs.set('limit', String(opts.limit));
|
|
60
|
+
if (opts.cursor)
|
|
61
|
+
qs.set('cursor', opts.cursor);
|
|
62
|
+
const s = qs.toString();
|
|
63
|
+
return this.http.req(`${P}/objects${s ? `?${s}` : ''}`);
|
|
64
|
+
}
|
|
65
|
+
/** Presigned URL (S3 backend only; 501 on the filesystem backend). */
|
|
66
|
+
presign(key, opts = {}) {
|
|
67
|
+
return this.http.req(`${P}/presign`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: JSON.stringify({ key, method: opts.method || 'GET', expiresIn: opts.expiresIn ?? 900 }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
createMultipart(key, contentType = 'application/octet-stream') {
|
|
73
|
+
return this.http.req(`${P}/uploads`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
body: JSON.stringify({ key, contentType }),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
uploadPart(uploadId, key, partNumber, body) {
|
|
79
|
+
const bytes = typeof body === 'string' ? new TextEncoder().encode(body) : body;
|
|
80
|
+
return this.http.upload(`${P}/uploads/${encodeURIComponent(uploadId)}/parts/${partNumber}?key=${encodeURIComponent(key)}`, bytes, { method: 'PUT', contentType: 'application/octet-stream' });
|
|
81
|
+
}
|
|
82
|
+
completeMultipart(uploadId, key, parts) {
|
|
83
|
+
return this.http.req(`${P}/uploads/${encodeURIComponent(uploadId)}/complete`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({ key, parts }),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
abortMultipart(uploadId, key) {
|
|
89
|
+
return this.http.req(`/v1/storage/uploads/${encodeURIComponent(uploadId)}?key=${encodeURIComponent(key)}`, { method: 'DELETE' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.StorageService = StorageService;
|
|
93
|
+
function encodeKey(key) {
|
|
94
|
+
return String(key || '')
|
|
95
|
+
.split('/')
|
|
96
|
+
.map((seg) => encodeURIComponent(seg))
|
|
97
|
+
.join('/');
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
// SPDX-FileCopyrightText: 2026 VKU.VIC
|
|
8
|
+
/*
|
|
9
|
+
* Copyright 2026 SynapseOne Contributors
|
|
10
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
* Object storage SDK tests: binary upload/download/head/list/multipart.
|
|
12
|
+
*/
|
|
13
|
+
const node_test_1 = require("node:test");
|
|
14
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
15
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
16
|
+
const index_1 = require("./index");
|
|
17
|
+
let server;
|
|
18
|
+
let base = '';
|
|
19
|
+
const objects = new Map();
|
|
20
|
+
let lastUploadId = '';
|
|
21
|
+
(0, node_test_1.before)(() => new Promise((resolve) => {
|
|
22
|
+
server = node_http_1.default
|
|
23
|
+
.createServer((req, res) => {
|
|
24
|
+
const url = new URL(req.url || '/', 'http://x');
|
|
25
|
+
const key = decodeURIComponent(url.pathname.replace('/v1/storage/objects/', ''));
|
|
26
|
+
const chunks = [];
|
|
27
|
+
req.on('data', (c) => chunks.push(c));
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
const body = Buffer.concat(chunks);
|
|
30
|
+
if (url.pathname === '/v1/storage/objects' && req.method === 'GET') {
|
|
31
|
+
res.setHeader('Content-Type', 'application/json');
|
|
32
|
+
res.end(JSON.stringify({ objects: [{ key: 'a.txt', size: 5, contentType: 'text/plain', etag: 'e', createdAt: 't', updatedAt: 't' }], nextCursor: null }));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (url.pathname === '/v1/storage/presign') {
|
|
36
|
+
res.setHeader('Content-Type', 'application/json');
|
|
37
|
+
res.end(JSON.stringify({ url: 'https://s3.example.com/x', method: 'GET', expiresIn: 900 }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (url.pathname === '/v1/storage/uploads') {
|
|
41
|
+
lastUploadId = 'u1';
|
|
42
|
+
res.statusCode = 201;
|
|
43
|
+
res.setHeader('Content-Type', 'application/json');
|
|
44
|
+
res.end(JSON.stringify({ uploadId: 'u1', key: 'k', contentType: 'application/octet-stream' }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (url.pathname.endsWith('/complete')) {
|
|
48
|
+
res.statusCode = 201;
|
|
49
|
+
res.setHeader('Content-Type', 'application/json');
|
|
50
|
+
res.end(JSON.stringify({ object: { key: 'k', size: 11, contentType: 'application/octet-stream', etag: 'e', createdAt: 't', updatedAt: 't' } }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (url.pathname.includes('/parts/')) {
|
|
54
|
+
res.setHeader('Content-Type', 'application/json');
|
|
55
|
+
res.end(JSON.stringify({ partNumber: 1, etag: 'p1', size: body.length }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (req.method === 'PUT') {
|
|
59
|
+
objects.set(key, { body, type: String(req.headers['content-type'] || '') });
|
|
60
|
+
res.statusCode = 201;
|
|
61
|
+
res.setHeader('Content-Type', 'application/json');
|
|
62
|
+
res.end(JSON.stringify({ object: { key, size: body.length, contentType: req.headers['content-type'], etag: 'e', createdAt: 't', updatedAt: 't' } }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (req.method === 'HEAD') {
|
|
66
|
+
const o = objects.get(key);
|
|
67
|
+
if (!o) {
|
|
68
|
+
res.statusCode = 404;
|
|
69
|
+
res.end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
res.setHeader('Content-Type', o.type);
|
|
73
|
+
res.setHeader('Content-Length', String(o.body.length));
|
|
74
|
+
res.setHeader('ETag', '"e"');
|
|
75
|
+
res.setHeader('x-object-key', key);
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (req.method === 'GET') {
|
|
80
|
+
const o = objects.get(key);
|
|
81
|
+
if (!o) {
|
|
82
|
+
res.statusCode = 404;
|
|
83
|
+
res.setHeader('Content-Type', 'application/json');
|
|
84
|
+
res.end('{"error":"not_found"}');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
res.setHeader('Content-Type', o.type);
|
|
88
|
+
res.end(o.body);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (req.method === 'DELETE') {
|
|
92
|
+
res.setHeader('Content-Type', 'application/json');
|
|
93
|
+
res.end(JSON.stringify({ ok: objects.delete(key), key }));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
res.statusCode = 404;
|
|
97
|
+
res.end('{}');
|
|
98
|
+
});
|
|
99
|
+
})
|
|
100
|
+
.listen(0, '127.0.0.1', () => {
|
|
101
|
+
const a = server.address();
|
|
102
|
+
base = `http://127.0.0.1:${typeof a === 'object' && a ? a.port : 0}`;
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
106
|
+
(0, node_test_1.after)(() => new Promise((resolve) => server.close(() => resolve())));
|
|
107
|
+
(0, node_test_1.describe)('SynapseObjectStorage SDK', () => {
|
|
108
|
+
(0, node_test_1.it)('uploads/downloads binary bodies without JSON wrapping', async () => {
|
|
109
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base });
|
|
110
|
+
const bytes = new Uint8Array([0, 1, 2, 254, 255]);
|
|
111
|
+
const meta = await c.storage.putObject('photos/a.png', bytes, { contentType: 'image/png' });
|
|
112
|
+
strict_1.default.equal(meta.size, 5);
|
|
113
|
+
const got = await c.storage.getObject('photos/a.png');
|
|
114
|
+
strict_1.default.deepEqual([...got], [0, 1, 2, 254, 255]);
|
|
115
|
+
});
|
|
116
|
+
(0, node_test_1.it)('reads HEAD metadata and lists objects', async () => {
|
|
117
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base });
|
|
118
|
+
await c.storage.putObject('a.txt', 'hello', { contentType: 'text/plain' });
|
|
119
|
+
const head = await c.storage.headObject('a.txt');
|
|
120
|
+
strict_1.default.equal(head?.size, 5);
|
|
121
|
+
strict_1.default.equal(head?.contentType, 'text/plain');
|
|
122
|
+
strict_1.default.equal(await c.storage.headObject('missing'), null);
|
|
123
|
+
const list = await c.storage.listObjects({ prefix: 'a' });
|
|
124
|
+
strict_1.default.equal(list.objects[0].key, 'a.txt');
|
|
125
|
+
});
|
|
126
|
+
(0, node_test_1.it)('presigns, deletes and runs multipart', async () => {
|
|
127
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base });
|
|
128
|
+
const pre = await c.storage.presign('a.txt', { expiresIn: 60 });
|
|
129
|
+
strict_1.default.equal(pre.expiresIn, 900); // mock returns its own value
|
|
130
|
+
const del = await c.storage.deleteObject('a.txt');
|
|
131
|
+
strict_1.default.equal(typeof del.ok, 'boolean');
|
|
132
|
+
const up = await c.storage.createMultipart('k');
|
|
133
|
+
strict_1.default.equal(up.uploadId, 'u1');
|
|
134
|
+
const part = await c.storage.uploadPart(lastUploadId, 'k', 1, new Uint8Array([1, 2, 3]));
|
|
135
|
+
strict_1.default.equal(part.etag, 'p1');
|
|
136
|
+
const done = await c.storage.completeMultipart(lastUploadId, 'k', [{ partNumber: 1, etag: 'p1' }]);
|
|
137
|
+
strict_1.default.equal(done.object.size, 11);
|
|
138
|
+
});
|
|
139
|
+
});
|
package/dist/stream.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
// SPDX-FileCopyrightText: 2026 VKU.VIC
|
|
8
|
+
/*
|
|
9
|
+
* Copyright 2026 SynapseOne Contributors
|
|
10
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
|
+
* Transport contract tests: non-JSON guard, retries, timeouts, streaming and
|
|
12
|
+
* typed health/readiness. Offline (local HTTP servers only).
|
|
13
|
+
*/
|
|
14
|
+
const node_test_1 = require("node:test");
|
|
15
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
16
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_module_1 = require("node:module");
|
|
19
|
+
const index_1 = require("./index");
|
|
20
|
+
const requireCjs = (0, node_module_1.createRequire)(__dirname);
|
|
21
|
+
let server;
|
|
22
|
+
let base = '';
|
|
23
|
+
let hits = new Map();
|
|
24
|
+
let slowMs = 0;
|
|
25
|
+
(0, node_test_1.before)(() => new Promise((resolve) => {
|
|
26
|
+
server = node_http_1.default
|
|
27
|
+
.createServer((req, res) => {
|
|
28
|
+
const url = req.url || '';
|
|
29
|
+
hits.set(url, (hits.get(url) || 0) + 1);
|
|
30
|
+
if (url === '/v1/orgs' && req.headers['x-test-html'] === '1') {
|
|
31
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
32
|
+
res.end('<!doctype html><html><body>console</body></html>');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (url === '/v1/orgs' && req.headers['x-test-html'] === '2') {
|
|
36
|
+
// HTML with a JSON content-type still has to be rejected by body sniffing.
|
|
37
|
+
res.setHeader('Content-Type', 'application/json');
|
|
38
|
+
res.end('<html><body>console</body></html>');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (url === '/v1/flaky') {
|
|
42
|
+
const n = hits.get(url) || 0;
|
|
43
|
+
if (n < 3) {
|
|
44
|
+
res.statusCode = 503;
|
|
45
|
+
res.setHeader('Content-Type', 'application/json');
|
|
46
|
+
res.end(JSON.stringify({ error: 'unavailable' }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.setHeader('Content-Type', 'application/json');
|
|
50
|
+
res.end(JSON.stringify({ ok: true, attempts: n }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (url === '/v1/retry-after') {
|
|
54
|
+
const n = hits.get(url) || 0;
|
|
55
|
+
if (n === 1) {
|
|
56
|
+
res.statusCode = 429;
|
|
57
|
+
res.setHeader('Retry-After', '0');
|
|
58
|
+
res.end('{}');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.setHeader('Content-Type', 'application/json');
|
|
62
|
+
res.end(JSON.stringify({ ok: true }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (url === '/v1/slow') {
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
res.setHeader('Content-Type', 'application/json');
|
|
68
|
+
res.end(JSON.stringify({ ok: true }));
|
|
69
|
+
}, slowMs);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (url === '/v1/never') {
|
|
73
|
+
return; // leave the socket open
|
|
74
|
+
}
|
|
75
|
+
if (url === '/ready') {
|
|
76
|
+
res.statusCode = 503;
|
|
77
|
+
res.setHeader('Content-Type', 'application/json');
|
|
78
|
+
res.end(JSON.stringify({ status: 'degraded', service: 'gateway', version: '0', upstreams: [{ name: 'base', ok: false }], time: 't' }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (url === '/health') {
|
|
82
|
+
res.setHeader('Content-Type', 'application/json');
|
|
83
|
+
res.end(JSON.stringify({ status: 'ok', service: 'gateway', version: '0', time: 't' }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (url === '/v1/inference/chat/completions') {
|
|
87
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
88
|
+
res.write('data: {"choices":[{"delta":{"content":"he"}}]}\n\n');
|
|
89
|
+
res.write('event: done\ndata: [DONE]\n\n');
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.setHeader('Content-Type', 'application/json');
|
|
94
|
+
res.end(JSON.stringify({ ok: true, path: url }));
|
|
95
|
+
})
|
|
96
|
+
.listen(0, '127.0.0.1', () => {
|
|
97
|
+
const a = server.address();
|
|
98
|
+
base = `http://127.0.0.1:${typeof a === 'object' && a ? a.port : 0}`;
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
}));
|
|
102
|
+
(0, node_test_1.after)(() => new Promise((resolve) => server.close(() => resolve())));
|
|
103
|
+
(0, node_test_1.describe)('transport', () => {
|
|
104
|
+
(0, node_test_1.it)('exposes the API gateway as the default URL constant', () => {
|
|
105
|
+
strict_1.default.equal(index_1.DEFAULT_BASE_URL, 'https://api.synapseone.space');
|
|
106
|
+
});
|
|
107
|
+
(0, node_test_1.it)('rejects a 200 text/html response with a typed error naming the base URL', async () => {
|
|
108
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
109
|
+
let err;
|
|
110
|
+
try {
|
|
111
|
+
await c.http.req('/v1/orgs', { headers: { 'x-test-html': '1' } });
|
|
112
|
+
strict_1.default.fail('expected a typed error');
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
err = e;
|
|
116
|
+
}
|
|
117
|
+
strict_1.default.ok(err instanceof index_1.SynapseOneError);
|
|
118
|
+
strict_1.default.equal(err.code, 'non_json_response');
|
|
119
|
+
strict_1.default.equal(err.status, 200);
|
|
120
|
+
strict_1.default.match(err.message, /api\.synapseone\.space/);
|
|
121
|
+
strict_1.default.equal(err.baseUrl, base);
|
|
122
|
+
});
|
|
123
|
+
(0, node_test_1.it)('rejects HTML bodies even when the content type says JSON', async () => {
|
|
124
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
125
|
+
await strict_1.default.rejects(() => c.http.req('/v1/orgs', { headers: { 'x-test-html': '2' } }), (e) => e instanceof index_1.SynapseOneError && e.code === 'non_json_response');
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)('retries idempotent requests on 503 and eventually succeeds', async () => {
|
|
128
|
+
hits.delete('/v1/flaky');
|
|
129
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 3, retryBaseMs: 1 });
|
|
130
|
+
const out = await c.http.req('/v1/flaky');
|
|
131
|
+
strict_1.default.equal(out.ok, true);
|
|
132
|
+
strict_1.default.equal(hits.get('/v1/flaky'), 3);
|
|
133
|
+
});
|
|
134
|
+
(0, node_test_1.it)('stops after maxAttempts and surfaces the last status', async () => {
|
|
135
|
+
hits.delete('/v1/flaky');
|
|
136
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 2, retryBaseMs: 1 });
|
|
137
|
+
await strict_1.default.rejects(() => c.http.req('/v1/flaky'), (e) => e.status === 503);
|
|
138
|
+
strict_1.default.equal(hits.get('/v1/flaky'), 2);
|
|
139
|
+
});
|
|
140
|
+
(0, node_test_1.it)('honours Retry-After', async () => {
|
|
141
|
+
hits.delete('/v1/retry-after');
|
|
142
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 2, retryBaseMs: 5 });
|
|
143
|
+
const out = await c.http.req('/v1/retry-after');
|
|
144
|
+
strict_1.default.equal(out.ok, true);
|
|
145
|
+
strict_1.default.equal(hits.get('/v1/retry-after'), 2);
|
|
146
|
+
});
|
|
147
|
+
(0, node_test_1.it)('does not retry non-idempotent POSTs unless asked', async () => {
|
|
148
|
+
hits.delete('/v1/flaky');
|
|
149
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 3, retryBaseMs: 1 });
|
|
150
|
+
await strict_1.default.rejects(() => c.http.req('/v1/flaky', { method: 'POST', body: '{}' }), (e) => e.status === 503);
|
|
151
|
+
strict_1.default.equal(hits.get('/v1/flaky'), 1);
|
|
152
|
+
});
|
|
153
|
+
(0, node_test_1.it)('applies a per-call timeout override', async () => {
|
|
154
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
155
|
+
const started = Date.now();
|
|
156
|
+
await strict_1.default.rejects(() => c.http.req('/v1/never', undefined, { timeoutMs: 50 }), (e) => e instanceof index_1.SynapseOneError && e.code === 'timeout');
|
|
157
|
+
strict_1.default.ok(Date.now() - started < 2000);
|
|
158
|
+
});
|
|
159
|
+
(0, node_test_1.it)('supports a caller AbortSignal', async () => {
|
|
160
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
161
|
+
const ctl = new AbortController();
|
|
162
|
+
setTimeout(() => ctl.abort(), 20);
|
|
163
|
+
await strict_1.default.rejects(() => c.http.req('/v1/never', undefined, { timeoutMs: 5000, signal: ctl.signal }), (e) => e.code === 'timeout' || e.code === 'network_error');
|
|
164
|
+
});
|
|
165
|
+
(0, node_test_1.it)('returns a degraded readiness payload instead of throwing (default)', async () => {
|
|
166
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
167
|
+
const ready = await c.health.ready();
|
|
168
|
+
strict_1.default.equal(ready.status, 'degraded');
|
|
169
|
+
strict_1.default.equal(ready.upstreams[0].name, 'base');
|
|
170
|
+
await strict_1.default.rejects(() => c.health.ready({ throwOnDegraded: true }), (e) => e.status === 503);
|
|
171
|
+
});
|
|
172
|
+
(0, node_test_1.it)('projects status() into ok/tier1/upstreams', async () => {
|
|
173
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
174
|
+
const status = await c.health.status();
|
|
175
|
+
strict_1.default.equal(status.ok, false);
|
|
176
|
+
strict_1.default.equal(status.tier1.reachable, true);
|
|
177
|
+
strict_1.default.equal(status.upstreams.length, 1);
|
|
178
|
+
});
|
|
179
|
+
(0, node_test_1.it)('streams SSE chat deltas without buffering', async () => {
|
|
180
|
+
const c = new index_1.SynapseOneClient({ baseUrl: base, maxAttempts: 1 });
|
|
181
|
+
const events = [];
|
|
182
|
+
for await (const ev of c.inference.chatStream([{ role: 'user', content: 'hi' }])) {
|
|
183
|
+
events.push(ev.data);
|
|
184
|
+
}
|
|
185
|
+
strict_1.default.equal(events.length, 2);
|
|
186
|
+
strict_1.default.equal((0, index_1.parseChatDelta)(events[0]), 'he');
|
|
187
|
+
strict_1.default.equal((0, index_1.parseChatDelta)(events[1]), null);
|
|
188
|
+
});
|
|
189
|
+
(0, node_test_1.it)('all documented package subpaths resolve', () => {
|
|
190
|
+
const pkg = requireCjs(node_path_1.default.join(__dirname, '..', 'package.json'));
|
|
191
|
+
const expected = ['.', './iam', './workflow', './ai', './lds', './base', './faststore', './queue', './stream', './inference', './health', './storage', './notifications'];
|
|
192
|
+
for (const sub of expected) {
|
|
193
|
+
strict_1.default.ok(pkg.exports[sub], `missing exports entry ${sub}`);
|
|
194
|
+
const target = node_path_1.default.join(__dirname, '..', pkg.exports[sub].require);
|
|
195
|
+
strict_1.default.doesNotThrow(() => requireCjs(target), `cannot require ${sub}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
package/dist/workflow.d.ts
CHANGED
|
@@ -54,9 +54,14 @@ export declare class WorkflowService {
|
|
|
54
54
|
* Fire a public inbound webhook through the gateway: `ANY /webhook/:path`.
|
|
55
55
|
* No auth is required; per-workflow auth/rate-limit/idempotency still apply.
|
|
56
56
|
* `path` is the webhook node's `parameters.path`, not a workflow id.
|
|
57
|
+
*
|
|
58
|
+
* The gateway allows 300s for `/webhook`; this call uses that budget unless
|
|
59
|
+
* `init.timeoutMs` overrides it.
|
|
57
60
|
*/
|
|
58
61
|
triggerWebhook(path: string, payload?: unknown, init?: {
|
|
59
62
|
method?: string;
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
signal?: AbortSignal;
|
|
60
65
|
}): Promise<unknown>;
|
|
61
66
|
}
|
|
62
67
|
/** Normalize GET /v1/credentials: the API returns `{data: []}`. */
|
|
@@ -79,4 +84,5 @@ export declare function unwrapExecutions(payload: any): any[];
|
|
|
79
84
|
export declare function triggerWebhook(webhookBaseUrl: string, path: string, payload?: unknown, init?: {
|
|
80
85
|
method?: string;
|
|
81
86
|
timeoutMs?: number;
|
|
87
|
+
signal?: AbortSignal;
|
|
82
88
|
}): Promise<any>;
|