@relayfile/core 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.
- package/dist/acl.d.ts +34 -0
- package/dist/acl.js +163 -0
- package/dist/events.d.ts +23 -0
- package/dist/events.js +39 -0
- package/dist/export.d.ts +16 -0
- package/dist/export.js +137 -0
- package/dist/files.d.ts +66 -0
- package/dist/files.js +240 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/operations.d.ts +20 -0
- package/dist/operations.js +94 -0
- package/dist/query.d.ts +30 -0
- package/dist/query.js +138 -0
- package/dist/semantics.d.ts +15 -0
- package/dist/semantics.js +90 -0
- package/dist/storage.d.ts +113 -0
- package/dist/storage.js +9 -0
- package/dist/tree.d.ts +35 -0
- package/dist/tree.js +106 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +32 -0
- package/dist/webhooks.d.ts +82 -0
- package/dist/webhooks.js +493 -0
- package/dist/writeback.d.ts +24 -0
- package/dist/writeback.js +148 -0
- package/package.json +39 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapter interface.
|
|
3
|
+
*
|
|
4
|
+
* All core logic functions accept a storage adapter — they never do I/O directly.
|
|
5
|
+
* Implementors provide the actual storage (DO SQLite, Go in-memory, Postgres, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Methods return plain data. Errors are signaled via return values, not exceptions.
|
|
8
|
+
*/
|
|
9
|
+
export interface FileRow {
|
|
10
|
+
path: string;
|
|
11
|
+
revision: string;
|
|
12
|
+
contentType: string;
|
|
13
|
+
/** Content body. May be inline or a ref key depending on storage backend. */
|
|
14
|
+
content: string;
|
|
15
|
+
encoding: string;
|
|
16
|
+
provider: string;
|
|
17
|
+
lastEditedAt: string;
|
|
18
|
+
semantics: FileSemantics;
|
|
19
|
+
}
|
|
20
|
+
export interface FileSemantics {
|
|
21
|
+
properties?: Record<string, string>;
|
|
22
|
+
relations?: string[];
|
|
23
|
+
permissions?: string[];
|
|
24
|
+
comments?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface EventRow {
|
|
27
|
+
eventId: string;
|
|
28
|
+
type: string;
|
|
29
|
+
path: string;
|
|
30
|
+
revision: string;
|
|
31
|
+
origin: string;
|
|
32
|
+
provider: string;
|
|
33
|
+
correlationId: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
export interface OperationRow {
|
|
37
|
+
opId: string;
|
|
38
|
+
path: string;
|
|
39
|
+
revision: string;
|
|
40
|
+
action: string;
|
|
41
|
+
provider: string;
|
|
42
|
+
status: string;
|
|
43
|
+
attemptCount: number;
|
|
44
|
+
lastError: string | null;
|
|
45
|
+
nextAttemptAt: string | null;
|
|
46
|
+
correlationId: string;
|
|
47
|
+
}
|
|
48
|
+
export interface WritebackItem {
|
|
49
|
+
id: string;
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
path: string;
|
|
52
|
+
revision: string;
|
|
53
|
+
correlationId: string;
|
|
54
|
+
}
|
|
55
|
+
export interface EnvelopeRow {
|
|
56
|
+
envelopeId: string;
|
|
57
|
+
workspaceId: string;
|
|
58
|
+
provider: string;
|
|
59
|
+
deliveryId: string;
|
|
60
|
+
deliveryIds?: string[];
|
|
61
|
+
receivedAt: string;
|
|
62
|
+
headers?: Record<string, string>;
|
|
63
|
+
payload: Record<string, unknown>;
|
|
64
|
+
correlationId: string;
|
|
65
|
+
status: string;
|
|
66
|
+
attemptCount: number;
|
|
67
|
+
lastError: string | null;
|
|
68
|
+
}
|
|
69
|
+
export interface PaginationOptions {
|
|
70
|
+
cursor?: string;
|
|
71
|
+
limit?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface Paginated<T> {
|
|
74
|
+
items: T[];
|
|
75
|
+
nextCursor: string | null;
|
|
76
|
+
}
|
|
77
|
+
export interface EnvelopeQueryOptions extends PaginationOptions {
|
|
78
|
+
workspaceId?: string;
|
|
79
|
+
provider?: string;
|
|
80
|
+
status?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface StorageAdapter {
|
|
83
|
+
getFile(path: string): FileRow | null;
|
|
84
|
+
listFiles(): FileRow[];
|
|
85
|
+
putFile(file: FileRow): void;
|
|
86
|
+
deleteFile(path: string): void;
|
|
87
|
+
loadFileContent?(file: FileRow): {
|
|
88
|
+
content: string;
|
|
89
|
+
encoding?: string;
|
|
90
|
+
} | string;
|
|
91
|
+
appendEvent(event: EventRow): void;
|
|
92
|
+
listEvents(options: PaginationOptions & {
|
|
93
|
+
provider?: string;
|
|
94
|
+
}): Paginated<EventRow>;
|
|
95
|
+
getRecentEvents(limit: number): EventRow[];
|
|
96
|
+
getOperation(opId: string): OperationRow | null;
|
|
97
|
+
putOperation(op: OperationRow): void;
|
|
98
|
+
listOperations(options: PaginationOptions & {
|
|
99
|
+
status?: string;
|
|
100
|
+
action?: string;
|
|
101
|
+
provider?: string;
|
|
102
|
+
}): Paginated<OperationRow>;
|
|
103
|
+
nextRevision(): string;
|
|
104
|
+
nextOperationId(): string;
|
|
105
|
+
nextEventId(): string;
|
|
106
|
+
enqueueWriteback(item: WritebackItem): void;
|
|
107
|
+
getPendingWritebacks(): WritebackItem[];
|
|
108
|
+
getEnvelopeByDelivery?(workspaceId: string, provider: string, deliveryId: string): EnvelopeRow | null;
|
|
109
|
+
putEnvelope?(envelope: EnvelopeRow): void;
|
|
110
|
+
putEnvelopeDeliveryAlias?(workspaceId: string, provider: string, deliveryId: string, envelopeId: string): void;
|
|
111
|
+
listEnvelopes?(options: EnvelopeQueryOptions): Paginated<EnvelopeRow>;
|
|
112
|
+
getWorkspaceId(): string;
|
|
113
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapter interface.
|
|
3
|
+
*
|
|
4
|
+
* All core logic functions accept a storage adapter — they never do I/O directly.
|
|
5
|
+
* Implementors provide the actual storage (DO SQLite, Go in-memory, Postgres, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Methods return plain data. Errors are signaled via return values, not exceptions.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree listing and directory traversal.
|
|
3
|
+
*
|
|
4
|
+
* Extract from workspace.ts:
|
|
5
|
+
* - listTree() / handleListTree()
|
|
6
|
+
* - directory inference from file paths
|
|
7
|
+
* - depth limiting
|
|
8
|
+
* - cursor pagination
|
|
9
|
+
* - ancestor/descendant path utilities
|
|
10
|
+
*/
|
|
11
|
+
import type { StorageAdapter, PaginationOptions } from "./storage.js";
|
|
12
|
+
import { type TokenClaims } from "./acl.js";
|
|
13
|
+
export interface TreeEntry {
|
|
14
|
+
path: string;
|
|
15
|
+
type: "file" | "dir";
|
|
16
|
+
revision?: string;
|
|
17
|
+
provider?: string;
|
|
18
|
+
size?: number;
|
|
19
|
+
updatedAt?: string;
|
|
20
|
+
propertyCount?: number;
|
|
21
|
+
relationCount?: number;
|
|
22
|
+
commentCount?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TreeResult {
|
|
25
|
+
path: string;
|
|
26
|
+
entries: TreeEntry[];
|
|
27
|
+
nextCursor: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface ListTreeOptions extends PaginationOptions {
|
|
30
|
+
path?: string;
|
|
31
|
+
depth?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function listTree(storage: StorageAdapter, options: ListTreeOptions, claims: TokenClaims | null): TreeResult;
|
|
34
|
+
export declare function ancestorDirectories(path: string): string[];
|
|
35
|
+
export declare function joinPath(dir: string, filename: string): string;
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree listing and directory traversal.
|
|
3
|
+
*
|
|
4
|
+
* Extract from workspace.ts:
|
|
5
|
+
* - listTree() / handleListTree()
|
|
6
|
+
* - directory inference from file paths
|
|
7
|
+
* - depth limiting
|
|
8
|
+
* - cursor pagination
|
|
9
|
+
* - ancestor/descendant path utilities
|
|
10
|
+
*/
|
|
11
|
+
import { filePermissionAllows, resolveFilePermissions, } from "./acl.js";
|
|
12
|
+
export function listTree(storage, options, claims) {
|
|
13
|
+
const base = normalizePath(options.path ?? "/");
|
|
14
|
+
const maxDepth = options.depth && options.depth > 0 ? options.depth : 1;
|
|
15
|
+
const workspaceId = storage.getWorkspaceId();
|
|
16
|
+
const entryMap = new Map();
|
|
17
|
+
for (const row of storage.listFiles()) {
|
|
18
|
+
const filePath = normalizePath(row.path);
|
|
19
|
+
if (filePath === base || !isWithinBase(filePath, base)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!filePermissionAllows(resolveFilePermissions(storage, filePath, true), workspaceId, claims)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const rest = filePath.slice(base === "/" ? 1 : base.length + 1);
|
|
26
|
+
if (!rest) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const parts = rest.split("/").filter(Boolean);
|
|
30
|
+
for (let level = 1; level <= Math.min(parts.length, maxDepth); level += 1) {
|
|
31
|
+
const childPath = joinPath(base, parts.slice(0, level).join("/"));
|
|
32
|
+
if (level === parts.length) {
|
|
33
|
+
entryMap.set(childPath, {
|
|
34
|
+
path: childPath,
|
|
35
|
+
type: "file",
|
|
36
|
+
revision: row.revision,
|
|
37
|
+
provider: row.provider,
|
|
38
|
+
size: encodedSize(row.content, row.encoding),
|
|
39
|
+
updatedAt: row.lastEditedAt,
|
|
40
|
+
propertyCount: Object.keys(row.semantics.properties ?? {}).length,
|
|
41
|
+
relationCount: row.semantics.relations?.length ?? 0,
|
|
42
|
+
commentCount: row.semantics.comments?.length ?? 0,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (!entryMap.has(childPath)) {
|
|
46
|
+
entryMap.set(childPath, {
|
|
47
|
+
path: childPath,
|
|
48
|
+
type: "dir",
|
|
49
|
+
revision: "dir",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let entries = Array.from(entryMap.values()).sort((left, right) => left.path.localeCompare(right.path));
|
|
55
|
+
if (options.cursor) {
|
|
56
|
+
const index = entries.findIndex((entry) => entry.path === options.cursor);
|
|
57
|
+
if (index >= 0) {
|
|
58
|
+
entries = entries.slice(index + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
path: base,
|
|
63
|
+
entries,
|
|
64
|
+
nextCursor: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function ancestorDirectories(path) {
|
|
68
|
+
const normalized = normalizePath(path);
|
|
69
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
70
|
+
const dirs = ["/"];
|
|
71
|
+
let current = "";
|
|
72
|
+
for (let index = 0; index < Math.max(0, parts.length - 1); index += 1) {
|
|
73
|
+
current = joinPath(current || "/", parts[index] ?? "");
|
|
74
|
+
dirs.push(current);
|
|
75
|
+
}
|
|
76
|
+
return dirs;
|
|
77
|
+
}
|
|
78
|
+
export function joinPath(dir, filename) {
|
|
79
|
+
const normalizedDir = normalizePath(dir);
|
|
80
|
+
return normalizedDir === "/"
|
|
81
|
+
? normalizePath(`/${filename}`)
|
|
82
|
+
: normalizePath(`${normalizedDir}/${filename}`);
|
|
83
|
+
}
|
|
84
|
+
function normalizePath(path) {
|
|
85
|
+
const trimmed = path.trim();
|
|
86
|
+
if (!trimmed) {
|
|
87
|
+
return "/";
|
|
88
|
+
}
|
|
89
|
+
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
90
|
+
return prefixed.length > 1 ? prefixed.replace(/\/+$/, "") : "/";
|
|
91
|
+
}
|
|
92
|
+
function isWithinBase(path, base) {
|
|
93
|
+
return base === "/" ? path.startsWith("/") : path.startsWith(`${base}/`);
|
|
94
|
+
}
|
|
95
|
+
function encodedSize(content, encoding) {
|
|
96
|
+
if (encoding === "base64") {
|
|
97
|
+
try {
|
|
98
|
+
return Uint8Array.from(atob(content), (char) => char.charCodeAt(0))
|
|
99
|
+
.byteLength;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return content.length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return new TextEncoder().encode(content).byteLength;
|
|
106
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions used across core modules.
|
|
3
|
+
*
|
|
4
|
+
* Centralises helpers that were previously copy-pasted in
|
|
5
|
+
* files.ts, query.ts, tree.ts, operations.ts, acl.ts, and writeback.ts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a file path: ensure leading slash, strip trailing slashes,
|
|
9
|
+
* and resolve `.` and `..` segments to prevent path traversal attacks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizePath(path: string): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility functions used across core modules.
|
|
3
|
+
*
|
|
4
|
+
* Centralises helpers that were previously copy-pasted in
|
|
5
|
+
* files.ts, query.ts, tree.ts, operations.ts, acl.ts, and writeback.ts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a file path: ensure leading slash, strip trailing slashes,
|
|
9
|
+
* and resolve `.` and `..` segments to prevent path traversal attacks.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePath(path) {
|
|
12
|
+
const trimmed = path.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
return "/";
|
|
15
|
+
}
|
|
16
|
+
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
17
|
+
// Split into segments and resolve . and .. to prevent path traversal
|
|
18
|
+
const segments = [];
|
|
19
|
+
for (const segment of prefixed.split("/")) {
|
|
20
|
+
if (segment === "." || segment === "") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (segment === "..") {
|
|
24
|
+
segments.pop();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
segments.push(segment);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const resolved = "/" + segments.join("/");
|
|
31
|
+
return resolved.length > 1 ? resolved.replace(/\/+$/, "") : "/";
|
|
32
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook envelope normalization and processing.
|
|
3
|
+
*
|
|
4
|
+
* Extract from workspace.ts:
|
|
5
|
+
* - generic webhook ingestion
|
|
6
|
+
* - envelope normalization
|
|
7
|
+
* - deduplication by delivery_id
|
|
8
|
+
* - coalescing within time window
|
|
9
|
+
* - suppression for loop prevention
|
|
10
|
+
* - stale event filtering
|
|
11
|
+
* - file materialization from webhook data
|
|
12
|
+
*
|
|
13
|
+
* This module stays pure. It only operates over plain storage records and
|
|
14
|
+
* optional callbacks for suppression/staleness policy.
|
|
15
|
+
*/
|
|
16
|
+
import type { StorageAdapter, EnvelopeRow, EnvelopeQueryOptions, Paginated, FileSemantics } from "./storage.js";
|
|
17
|
+
export interface IngestWebhookInput {
|
|
18
|
+
provider: string;
|
|
19
|
+
eventType?: string;
|
|
20
|
+
path?: string;
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
deliveryId?: string;
|
|
23
|
+
timestamp?: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
correlationId?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface IngestResult {
|
|
28
|
+
status: string;
|
|
29
|
+
envelopeId: string;
|
|
30
|
+
correlationId: string;
|
|
31
|
+
}
|
|
32
|
+
export interface EnvelopeEvent {
|
|
33
|
+
type: string;
|
|
34
|
+
path: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
content?: string;
|
|
37
|
+
contentType?: string;
|
|
38
|
+
encoding?: string;
|
|
39
|
+
semantics?: FileSemantics;
|
|
40
|
+
data?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface ApplyEnvelopeOptions {
|
|
43
|
+
now?: () => string;
|
|
44
|
+
shouldSuppress?: (envelope: EnvelopeRow, event: EnvelopeEvent) => boolean;
|
|
45
|
+
isStale?: (envelope: EnvelopeRow, event: EnvelopeEvent) => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Optional ACL enforcement callback. When provided, called before file
|
|
48
|
+
* writes with the target path. Return `true` to allow, `false` to deny.
|
|
49
|
+
* If denied, the envelope is marked as "ignored".
|
|
50
|
+
*/
|
|
51
|
+
isPathWriteAllowed?: (path: string) => boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface ApplyEnvelopeResult {
|
|
54
|
+
status: "processed" | "ignored" | "suppressed" | "stale" | "rejected";
|
|
55
|
+
eventType: string | null;
|
|
56
|
+
path: string | null;
|
|
57
|
+
revision: string | null;
|
|
58
|
+
reason?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface IngestWebhookOptions {
|
|
61
|
+
now?: () => string;
|
|
62
|
+
generateEnvelopeId?: () => string;
|
|
63
|
+
coalesceWindowMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Optional signature verification callback. When provided, called with the
|
|
66
|
+
* raw input before processing. Return `true` if the payload signature is
|
|
67
|
+
* valid, `false` to reject the webhook. Callers should implement
|
|
68
|
+
* HMAC/RSA verification in this callback at the HTTP handler layer.
|
|
69
|
+
*/
|
|
70
|
+
signatureVerifier?: (input: IngestWebhookInput) => boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface WebhookStorageAdapter extends StorageAdapter {
|
|
73
|
+
getEnvelopeByDelivery(workspaceId: string, provider: string, deliveryId: string): EnvelopeRow | null;
|
|
74
|
+
putEnvelope(envelope: EnvelopeRow): void;
|
|
75
|
+
putEnvelopeDeliveryAlias?(workspaceId: string, provider: string, deliveryId: string, envelopeId: string): void;
|
|
76
|
+
listEnvelopes(options: EnvelopeQueryOptions): Paginated<EnvelopeRow>;
|
|
77
|
+
}
|
|
78
|
+
export declare function ingestWebhook(storage: StorageAdapter, input: IngestWebhookInput, options?: IngestWebhookOptions): IngestResult;
|
|
79
|
+
export declare function normalizeEnvelope(input: IngestWebhookInput, now?: () => string): Partial<EnvelopeRow>;
|
|
80
|
+
export declare function normalizeEnvelopeEvent(envelope: Pick<EnvelopeRow, "payload" | "receivedAt">): EnvelopeEvent | null;
|
|
81
|
+
export declare function normalizeEnvelopePath(envelope: Pick<EnvelopeRow, "payload">): string | null;
|
|
82
|
+
export declare function applyWebhookEnvelope(storage: StorageAdapter, envelope: EnvelopeRow, options?: ApplyEnvelopeOptions): ApplyEnvelopeResult;
|