@delma/fylo 2.1.0 → 2.1.1

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 (99) hide show
  1. package/README.md +27 -0
  2. package/dist/adapters/cipher.js +155 -0
  3. package/dist/adapters/cipher.js.map +1 -0
  4. package/dist/core/collection.js +6 -0
  5. package/dist/core/collection.js.map +1 -0
  6. package/{src/core/directory.ts → dist/core/directory.js} +28 -35
  7. package/dist/core/directory.js.map +1 -0
  8. package/dist/core/doc-id.js +15 -0
  9. package/dist/core/doc-id.js.map +1 -0
  10. package/dist/core/extensions.js +16 -0
  11. package/dist/core/extensions.js.map +1 -0
  12. package/dist/core/format.js +355 -0
  13. package/dist/core/format.js.map +1 -0
  14. package/dist/core/parser.js +764 -0
  15. package/dist/core/parser.js.map +1 -0
  16. package/dist/core/query.js +47 -0
  17. package/dist/core/query.js.map +1 -0
  18. package/dist/engines/s3-files/documents.js +62 -0
  19. package/dist/engines/s3-files/documents.js.map +1 -0
  20. package/dist/engines/s3-files/filesystem.js +165 -0
  21. package/dist/engines/s3-files/filesystem.js.map +1 -0
  22. package/dist/engines/s3-files/query.js +235 -0
  23. package/dist/engines/s3-files/query.js.map +1 -0
  24. package/dist/engines/s3-files/types.js +2 -0
  25. package/dist/engines/s3-files/types.js.map +1 -0
  26. package/dist/engines/s3-files.js +629 -0
  27. package/dist/engines/s3-files.js.map +1 -0
  28. package/dist/engines/types.js +2 -0
  29. package/dist/engines/types.js.map +1 -0
  30. package/dist/index.js +562 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/sync.js +18 -0
  33. package/dist/sync.js.map +1 -0
  34. package/{src → dist}/types/fylo.d.ts +14 -1
  35. package/package.json +2 -2
  36. package/.env.example +0 -16
  37. package/.github/copilot-instructions.md +0 -3
  38. package/.github/prompts/release.prompt.md +0 -10
  39. package/.github/workflows/ci.yml +0 -37
  40. package/.github/workflows/publish.yml +0 -91
  41. package/.prettierrc +0 -7
  42. package/AGENTS.md +0 -3
  43. package/CLAUDE.md +0 -3
  44. package/eslint.config.js +0 -32
  45. package/src/CLI +0 -39
  46. package/src/adapters/cipher.ts +0 -180
  47. package/src/core/collection.ts +0 -5
  48. package/src/core/extensions.ts +0 -21
  49. package/src/core/format.ts +0 -457
  50. package/src/core/parser.ts +0 -901
  51. package/src/core/query.ts +0 -53
  52. package/src/engines/s3-files/documents.ts +0 -65
  53. package/src/engines/s3-files/filesystem.ts +0 -172
  54. package/src/engines/s3-files/query.ts +0 -291
  55. package/src/engines/s3-files/types.ts +0 -42
  56. package/src/engines/s3-files.ts +0 -769
  57. package/src/engines/types.ts +0 -21
  58. package/src/index.ts +0 -632
  59. package/src/sync.ts +0 -58
  60. package/tests/collection/truncate.test.js +0 -36
  61. package/tests/data.js +0 -97
  62. package/tests/helpers/root.js +0 -7
  63. package/tests/integration/aws-s3-files.canary.test.js +0 -22
  64. package/tests/integration/create.test.js +0 -39
  65. package/tests/integration/delete.test.js +0 -97
  66. package/tests/integration/edge-cases.test.js +0 -162
  67. package/tests/integration/encryption.test.js +0 -148
  68. package/tests/integration/export.test.js +0 -46
  69. package/tests/integration/join-modes.test.js +0 -154
  70. package/tests/integration/nested.test.js +0 -144
  71. package/tests/integration/operators.test.js +0 -136
  72. package/tests/integration/read.test.js +0 -123
  73. package/tests/integration/rollback.test.js +0 -30
  74. package/tests/integration/s3-files.performance.test.js +0 -75
  75. package/tests/integration/s3-files.test.js +0 -205
  76. package/tests/integration/sync.test.js +0 -154
  77. package/tests/integration/update.test.js +0 -105
  78. package/tests/mocks/cipher.js +0 -40
  79. package/tests/schemas/album.d.ts +0 -5
  80. package/tests/schemas/album.json +0 -5
  81. package/tests/schemas/comment.d.ts +0 -7
  82. package/tests/schemas/comment.json +0 -7
  83. package/tests/schemas/photo.d.ts +0 -7
  84. package/tests/schemas/photo.json +0 -7
  85. package/tests/schemas/post.d.ts +0 -6
  86. package/tests/schemas/post.json +0 -6
  87. package/tests/schemas/tip.d.ts +0 -7
  88. package/tests/schemas/tip.json +0 -7
  89. package/tests/schemas/todo.d.ts +0 -6
  90. package/tests/schemas/todo.json +0 -6
  91. package/tests/schemas/user.d.ts +0 -23
  92. package/tests/schemas/user.json +0 -23
  93. package/tsconfig.json +0 -21
  94. package/tsconfig.typecheck.json +0 -31
  95. /package/{src → dist}/types/bun-runtime.d.ts +0 -0
  96. /package/{src → dist}/types/index.d.ts +0 -0
  97. /package/{src → dist}/types/node-runtime.d.ts +0 -0
  98. /package/{src → dist}/types/query.d.ts +0 -0
  99. /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@delma/fylo",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/types/index.d.ts",
6
6
  "bin": {
7
7
  "fylo.query": "./dist/cli/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc",
10
+ "build": "rm -rf dist && tsc && mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
11
11
  "test": "bun test",
12
12
  "typecheck": "tsc -p tsconfig.typecheck.json",
13
13
  "lint": "prettier --check \"src/**/*.{ts,d.ts}\" \"tests/**/*.js\" README.md",
package/.env.example DELETED
@@ -1,16 +0,0 @@
1
- LOGGING=
2
- STRICT=
3
- S3_REGION=ca-central-1
4
- S3_ACCESS_KEY_ID=HELLO
5
- S3_SECRET_ACCESS_KEY=WORLD
6
- S3_ENDPOINT=https//example.com
7
- BUCKET_PREFIX="byos-test"
8
- SCHEMA_DIR=/path/to/schema/dir
9
- REDIS_URL=redis://localhost:6379
10
- REDIS_CONN_TIMEOUT=5000
11
- REDIS_IDLE_TIMEOUT=30000
12
- REDIS_AUTO_RECONNECT=
13
- REDIS_MAX_RETRIES=10
14
- REDIS_ENABLE_OFFLINE_QUEUE=
15
- REDIS_ENABLE_AUTO_PIPELINING=
16
- REDIS_TLS=
@@ -1,3 +0,0 @@
1
- Follow the shared workspace instructions in `../../INSTRUCTIONS.md`, shared context in `../../MEMORY.md`, and shared release process in `../../RELEASE.md`.
2
-
3
- If a repo-local instruction file adds stricter rules, follow the repo-local rule.
@@ -1,10 +0,0 @@
1
- ---
2
- description: "Follow the shared workspace release process"
3
- agent: "agent"
4
- tools: [runInTerminal]
5
- ---
6
- Follow the shared workspace release process in [../../../RELEASE.md](../../../RELEASE.md).
7
-
8
- Use the repo's actual default branch. Do not assume it is `main`.
9
-
10
- If repo-local workflows impose stricter checks or branch requirements, follow the repo-local workflow and then update the shared release guide later if that rule becomes the new standard.
@@ -1,37 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [release/*]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- test:
11
- name: Test (Bun ${{ matrix.bun-version }})
12
- runs-on: ubuntu-latest
13
-
14
- strategy:
15
- matrix:
16
- bun-version: [latest, 1.2.x]
17
-
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v4
21
-
22
- - name: Setup Bun
23
- uses: oven-sh/setup-bun@v2
24
- with:
25
- bun-version: ${{ matrix.bun-version }}
26
-
27
- - name: Install dependencies
28
- run: bun install --frozen-lockfile
29
-
30
- - name: Type check
31
- run: bun run typecheck
32
-
33
- - name: Lint
34
- run: bun run lint
35
-
36
- - name: Run tests
37
- run: bun test
@@ -1,91 +0,0 @@
1
- name: Publish
2
-
3
- on:
4
- push:
5
- branches: [release/*]
6
-
7
- jobs:
8
- test:
9
- name: Test before publish
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout
14
- uses: actions/checkout@v4
15
-
16
- - name: Setup Bun
17
- uses: oven-sh/setup-bun@v2
18
- with:
19
- bun-version: latest
20
-
21
- - name: Install dependencies
22
- run: bun install --frozen-lockfile
23
-
24
- - name: Type check
25
- run: bun run typecheck
26
-
27
- - name: Lint
28
- run: bun run lint
29
-
30
- - name: Run tests
31
- run: bun test
32
-
33
- publish:
34
- name: Publish to npm
35
- runs-on: ubuntu-latest
36
- needs: test
37
- permissions:
38
- contents: write # create git tags
39
- id-token: write # npm provenance
40
-
41
- steps:
42
- - name: Checkout
43
- uses: actions/checkout@v4
44
-
45
- - name: Setup Bun
46
- uses: oven-sh/setup-bun@v2
47
- with:
48
- bun-version: latest
49
-
50
- - name: Install dependencies
51
- run: bun install --frozen-lockfile
52
-
53
- - name: Setup Node and upgrade npm
54
- uses: actions/setup-node@v4
55
- with:
56
- node-version: '20'
57
-
58
- - name: Upgrade npm
59
- run: npm install -g npm@latest
60
-
61
- - name: Resolve version from branch
62
- id: version
63
- run: |
64
- BRANCH="${GITHUB_REF#refs/heads/}"
65
- VERSION="${BRANCH#release/}"
66
- PKG_VERSION=$(bun -e "console.log(require('./package.json').version)")
67
- if [ "$VERSION" != "$PKG_VERSION" ]; then
68
- echo "Branch version ($VERSION) does not match package.json ($PKG_VERSION). Skipping publish."
69
- exit 1
70
- fi
71
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
72
-
73
- - name: Publish to npm (Trusted Publishing via OIDC)
74
- run: |
75
- echo "registry=https://registry.npmjs.org/" >> ~/.npmrc
76
- npm publish --access public --provenance
77
-
78
- - name: Create and push version tag
79
- run: |
80
- git config user.name "github-actions[bot]"
81
- git config user.email "github-actions[bot]@users.noreply.github.com"
82
- git tag -f -a "v${{ steps.version.outputs.version }}" -m "v${{ steps.version.outputs.version }}"
83
- git push origin "v${{ steps.version.outputs.version }}" --force
84
-
85
- - name: Create GitHub release
86
- run: |
87
- gh release create "v${{ steps.version.outputs.version }}" \
88
- --title "v${{ steps.version.outputs.version }}" \
89
- --generate-notes
90
- env:
91
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "semi": false,
3
- "singleQuote": true,
4
- "tabWidth": 4,
5
- "trailingComma": "none",
6
- "printWidth": 100
7
- }
package/AGENTS.md DELETED
@@ -1,3 +0,0 @@
1
- Follow the shared workspace instructions in [../INSTRUCTIONS.md](../INSTRUCTIONS.md), shared context in [../MEMORY.md](../MEMORY.md), and shared release process in [../RELEASE.md](../RELEASE.md).
2
-
3
- Repo-local rules may add to or override the shared files when explicitly stated.
package/CLAUDE.md DELETED
@@ -1,3 +0,0 @@
1
- Follow the shared workspace instructions in [../INSTRUCTIONS.md](../INSTRUCTIONS.md), shared context in [../MEMORY.md](../MEMORY.md), and shared release process in [../RELEASE.md](../RELEASE.md).
2
-
3
- Repo-local rules may add to or override the shared files when explicitly stated.
package/eslint.config.js DELETED
@@ -1,32 +0,0 @@
1
- import tsPlugin from '@typescript-eslint/eslint-plugin'
2
- import tsParser from '@typescript-eslint/parser'
3
- import prettierConfig from 'eslint-config-prettier'
4
-
5
- export default [
6
- {
7
- files: ['src/**/*.ts'],
8
- languageOptions: {
9
- parser: tsParser,
10
- parserOptions: {}
11
- },
12
- plugins: {
13
- '@typescript-eslint': tsPlugin
14
- },
15
- rules: {
16
- ...tsPlugin.configs['recommended'].rules,
17
- '@typescript-eslint/no-explicit-any': 'warn',
18
- '@typescript-eslint/explicit-function-return-type': 'warn',
19
- '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
20
- }
21
- },
22
- {
23
- files: ['tests/**/*.js'],
24
- rules: {
25
- 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
26
- }
27
- },
28
- prettierConfig,
29
- {
30
- ignores: ['bin/**', 'node_modules/**', '**/*.d.ts']
31
- }
32
- ]
package/src/CLI DELETED
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env bun
2
- /// <reference path="./types/index.d.ts" />
3
- import Silo from '.'
4
-
5
- const SQL = process.argv[process.argv.length - 1]
6
-
7
- const op = SQL.match(
8
- /^((?:SELECT|select)|(?:INSERT|insert)|(?:UPDATE|update)|(?:DELETE|delete)|(?:CREATE|create)|(?:DROP|drop))/i
9
- )
10
-
11
- if (!op) throw new Error('Missing SQL Operation')
12
-
13
- const res = await new Silo().executeSQL(SQL)
14
-
15
- const cmnd = op.shift()!
16
-
17
- switch (cmnd.toUpperCase()) {
18
- case 'CREATE':
19
- console.log('Successfully created schema')
20
- break
21
- case 'DROP':
22
- console.log('Successfully dropped schema')
23
- break
24
- case 'SELECT':
25
- if (typeof res === 'object' && !Array.isArray(res)) console.format(res)
26
- else console.log(res)
27
- break
28
- case 'INSERT':
29
- console.log(res)
30
- break
31
- case 'UPDATE':
32
- console.log(`Successfully updated ${res} document(s)`)
33
- break
34
- case 'DELETE':
35
- console.log(`Successfully deleted ${res} document(s)`)
36
- break
37
- default:
38
- throw new Error('Invalid Operation: ' + cmnd)
39
- }
@@ -1,180 +0,0 @@
1
- /**
2
- * AES-256-CBC encryption adapter for field-level value encryption.
3
- *
4
- * Two modes are supported via the `deterministic` flag on `encrypt()`:
5
- *
6
- * - **Random IV (default)**: A cryptographically random IV is generated per
7
- * encryption operation. Identical plaintexts produce different ciphertexts.
8
- * Use this for fields that do not need exact-match ($eq/$ne) queries.
9
- *
10
- * - **Deterministic IV (opt-in)**: The IV is derived from HMAC-SHA256 of the
11
- * plaintext, so identical values always produce identical ciphertext. This
12
- * enables exact-match queries on encrypted fields but leaks equality — an
13
- * observer can determine which records share field values without decrypting.
14
- * Use only when $eq/$ne queries on encrypted fields are required.
15
- *
16
- * Encrypted fields are declared per-collection in JSON schema files via the
17
- * `$encrypted` array. The encryption key is sourced from `ENCRYPTION_KEY` env var.
18
- * Set `CIPHER_SALT` to a unique random value to prevent cross-deployment attacks.
19
- */
20
-
21
- export class Cipher {
22
- private static key: CryptoKey | null = null
23
- private static hmacKey: CryptoKey | null = null
24
-
25
- /** Per-collection encrypted field sets, loaded from schema `$encrypted` arrays. */
26
- private static collections: Map<string, Set<string>> = new Map()
27
-
28
- static isConfigured(): boolean {
29
- return Cipher.key !== null
30
- }
31
-
32
- static hasEncryptedFields(collection: string): boolean {
33
- const fields = Cipher.collections.get(collection)
34
- return !!fields && fields.size > 0
35
- }
36
-
37
- static isEncryptedField(collection: string, field: string): boolean {
38
- const fields = Cipher.collections.get(collection)
39
- if (!fields || fields.size === 0) return false
40
-
41
- for (const pattern of fields) {
42
- if (field === pattern) return true
43
- // Support nested: encrypting "address" encrypts "address/city" etc.
44
- if (field.startsWith(`${pattern}/`)) return true
45
- }
46
-
47
- return false
48
- }
49
-
50
- /**
51
- * Registers encrypted fields for a collection (from schema `$encrypted` array).
52
- */
53
- static registerFields(collection: string, fields: string[]): void {
54
- if (fields.length > 0) {
55
- Cipher.collections.set(collection, new Set(fields))
56
- }
57
- }
58
-
59
- /**
60
- * Derives AES + HMAC keys from a secret string. Called once at startup.
61
- */
62
- static async configure(secret: string): Promise<void> {
63
- const encoder = new TextEncoder()
64
- const keyMaterial = await crypto.subtle.importKey(
65
- 'raw',
66
- encoder.encode(secret),
67
- 'PBKDF2',
68
- false,
69
- ['deriveBits']
70
- )
71
-
72
- const cipherSalt = process.env.CIPHER_SALT
73
- if (!cipherSalt) {
74
- console.warn(
75
- 'CIPHER_SALT is not set. Using default salt is insecure for multi-deployment use. Set CIPHER_SALT to a unique random value.'
76
- )
77
- }
78
-
79
- // Derive 48 bytes: 32 for AES key + 16 for HMAC key
80
- const bits = await crypto.subtle.deriveBits(
81
- {
82
- name: 'PBKDF2',
83
- salt: encoder.encode(cipherSalt ?? 'fylo-cipher'),
84
- iterations: 100000,
85
- hash: 'SHA-256'
86
- },
87
- keyMaterial,
88
- 384
89
- )
90
-
91
- const derived = new Uint8Array(bits)
92
-
93
- Cipher.key = await crypto.subtle.importKey(
94
- 'raw',
95
- derived.slice(0, 32),
96
- { name: 'AES-CBC' },
97
- false,
98
- ['encrypt', 'decrypt']
99
- )
100
-
101
- Cipher.hmacKey = await crypto.subtle.importKey(
102
- 'raw',
103
- derived.slice(32),
104
- { name: 'HMAC', hash: 'SHA-256' },
105
- false,
106
- ['sign']
107
- )
108
- }
109
-
110
- static reset(): void {
111
- Cipher.key = null
112
- Cipher.hmacKey = null
113
- Cipher.collections = new Map()
114
- }
115
-
116
- /**
117
- * Deterministic IV from HMAC-SHA256 of plaintext, truncated to 16 bytes.
118
- */
119
- private static async deriveIV(plaintext: string): Promise<Uint8Array> {
120
- const encoder = new TextEncoder()
121
- const sig = await crypto.subtle.sign('HMAC', Cipher.hmacKey!, encoder.encode(plaintext))
122
- return new Uint8Array(sig).slice(0, 16)
123
- }
124
-
125
- /**
126
- * Encrypts a value. Returns a URL-safe base64 string (no slashes).
127
- *
128
- * @param value - The plaintext to encrypt.
129
- * @param deterministic - When true, derives IV from HMAC of plaintext (same
130
- * input always produces same ciphertext). Required for $eq/$ne queries on
131
- * encrypted fields. Defaults to false (random IV per operation).
132
- */
133
- static async encrypt(value: string, deterministic = false): Promise<string> {
134
- if (!Cipher.key) throw new Error('Cipher not configured — set ENCRYPTION_KEY env var')
135
-
136
- const iv = deterministic
137
- ? await Cipher.deriveIV(value)
138
- : crypto.getRandomValues(new Uint8Array(16))
139
- const encoder = new TextEncoder()
140
-
141
- const encrypted = await crypto.subtle.encrypt(
142
- { name: 'AES-CBC', iv: iv as any },
143
- Cipher.key,
144
- encoder.encode(value)
145
- )
146
-
147
- // Concatenate IV + ciphertext and encode as URL-safe base64
148
- const combined = new Uint8Array(iv.length + encrypted.byteLength)
149
- combined.set(iv)
150
- combined.set(new Uint8Array(encrypted), iv.length)
151
-
152
- return btoa(String.fromCharCode(...combined))
153
- .replace(/\+/g, '-')
154
- .replace(/\//g, '_')
155
- .replace(/=+$/, '')
156
- }
157
-
158
- /**
159
- * Decrypts a URL-safe base64 encoded value back to plaintext.
160
- */
161
- static async decrypt(encoded: string): Promise<string> {
162
- if (!Cipher.key) throw new Error('Cipher not configured — set ENCRYPTION_KEY env var')
163
-
164
- // Restore standard base64
165
- const b64 = encoded.replace(/-/g, '+').replace(/_/g, '/')
166
- const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4)
167
-
168
- const combined = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0))
169
- const iv = combined.slice(0, 16)
170
- const ciphertext = combined.slice(16)
171
-
172
- const decrypted = await crypto.subtle.decrypt(
173
- { name: 'AES-CBC', iv },
174
- Cipher.key,
175
- ciphertext
176
- )
177
-
178
- return new TextDecoder().decode(decrypted)
179
- }
180
- }
@@ -1,5 +0,0 @@
1
- export function validateCollectionName(collection: string): void {
2
- if (!/^[a-z0-9][a-z0-9\-]*[a-z0-9]$/.test(collection)) {
3
- throw new Error('Invalid collection name')
4
- }
5
- }
@@ -1,21 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
- Object.appendGroup = function (
4
- target: Record<string, any>,
5
- source: Record<string, any>
6
- ): Record<string, any> {
7
- const result = { ...target }
8
-
9
- for (const [sourceId, sourceGroup] of Object.entries(source)) {
10
- if (!result[sourceId]) {
11
- result[sourceId] = sourceGroup
12
- continue
13
- }
14
-
15
- for (const [groupId, groupDoc] of Object.entries(sourceGroup)) {
16
- result[sourceId][groupId] = groupDoc
17
- }
18
- }
19
-
20
- return result
21
- }