@lark-sh/client 0.1.15 → 0.1.17

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.
@@ -43,70 +43,6 @@ __export(fb_v8_exports, {
43
43
  });
44
44
  module.exports = __toCommonJS(fb_v8_exports);
45
45
 
46
- // src/protocol/constants.ts
47
- var OnDisconnectAction = {
48
- SET: "s",
49
- UPDATE: "u",
50
- DELETE: "d",
51
- CANCEL: "c"
52
- };
53
- var ErrorCode = {
54
- PERMISSION_DENIED: "permission_denied",
55
- INVALID_DATA: "invalid_data",
56
- NOT_FOUND: "not_found",
57
- INVALID_PATH: "invalid_path",
58
- INVALID_OPERATION: "invalid_operation",
59
- INTERNAL_ERROR: "internal_error",
60
- CONDITION_FAILED: "condition_failed",
61
- INVALID_QUERY: "invalid_query",
62
- AUTH_REQUIRED: "auth_required"
63
- };
64
- var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
65
-
66
- // src/connection/Coordinator.ts
67
- var Coordinator = class {
68
- constructor(baseUrl = DEFAULT_COORDINATOR_URL) {
69
- this.baseUrl = baseUrl.replace(/\/$/, "");
70
- }
71
- /**
72
- * Request connection details from the coordinator.
73
- *
74
- * @param database - Database ID in format "project/database"
75
- * @param options - Either a user token or anonymous flag
76
- * @returns Connection details including host, port, and connection token
77
- */
78
- async connect(database, options = {}) {
79
- const body = { database };
80
- if (options.token) {
81
- body.token = options.token;
82
- } else if (options.anonymous) {
83
- body.anonymous = true;
84
- } else {
85
- body.anonymous = true;
86
- }
87
- const response = await fetch(`${this.baseUrl}/connect`, {
88
- method: "POST",
89
- headers: {
90
- "Content-Type": "application/json"
91
- },
92
- body: JSON.stringify(body)
93
- });
94
- if (!response.ok) {
95
- let errorMessage = `Coordinator request failed: ${response.status}`;
96
- try {
97
- const errorBody = await response.json();
98
- if (errorBody.message) {
99
- errorMessage = errorBody.message;
100
- }
101
- } catch {
102
- }
103
- throw new Error(errorMessage);
104
- }
105
- const data = await response.json();
106
- return data;
107
- }
108
- };
109
-
110
46
  // src/connection/WebSocketTransport.ts
111
47
  var import_ws = __toESM(require("ws"));
112
48
  var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : import_ws.default;
@@ -1473,7 +1409,6 @@ var View = class {
1473
1409
  /**
1474
1410
  * Check if this View loads all data (no limits, no range filters).
1475
1411
  * A View that loads all data can serve as a "complete" cache for child paths.
1476
- * This matches Firebase's loadsAllData() semantics.
1477
1412
  */
1478
1413
  loadsAllData() {
1479
1414
  if (!this.queryParams) return true;
@@ -1889,8 +1824,8 @@ var SubscriptionManager = class {
1889
1824
  /**
1890
1825
  * Detect and fire child_moved events for children that changed position OR sort value.
1891
1826
  *
1892
- * Firebase fires child_moved for ANY priority/sort value change, regardless of whether
1893
- * the position actually changes. This is Case 2003 behavior.
1827
+ * child_moved fires for ANY priority/sort value change, regardless of whether
1828
+ * the position actually changes.
1894
1829
  *
1895
1830
  * IMPORTANT: child_moved should only fire for children whose VALUE or PRIORITY changed.
1896
1831
  * Children that are merely "displaced" by another child moving should NOT fire child_moved.
@@ -2093,9 +2028,6 @@ var SubscriptionManager = class {
2093
2028
  * A complete View has no limits and no range filters, so it contains all data
2094
2029
  * for its subtree. Child subscriptions can use the ancestor's data instead
2095
2030
  * of creating their own server subscription.
2096
- *
2097
- * This matches Firebase's behavior where child listeners don't need their own
2098
- * server subscription if an ancestor has an unlimited listener.
2099
2031
  */
2100
2032
  hasAncestorCompleteView(path) {
2101
2033
  const normalized = normalizePath(path);
@@ -2221,8 +2153,8 @@ var SubscriptionManager = class {
2221
2153
  /**
2222
2154
  * Recursively sort object keys for consistent comparison.
2223
2155
  * This ensures {a:1, b:2} and {b:2, a:1} compare as equal.
2224
- * Uses simple alphabetical sorting (not Firebase key sorting) since we're
2225
- * just checking data equality, not display order.
2156
+ * Uses simple alphabetical sorting since we're just checking data equality,
2157
+ * not display order.
2226
2158
  */
2227
2159
  sortKeysForComparison(value) {
2228
2160
  if (value === null || typeof value !== "object") {
@@ -2816,6 +2748,26 @@ var SubscriptionManager = class {
2816
2748
  }
2817
2749
  };
2818
2750
 
2751
+ // src/protocol/constants.ts
2752
+ var OnDisconnectAction = {
2753
+ SET: "s",
2754
+ UPDATE: "u",
2755
+ DELETE: "d",
2756
+ CANCEL: "c"
2757
+ };
2758
+ var ErrorCode = {
2759
+ PERMISSION_DENIED: "permission_denied",
2760
+ INVALID_DATA: "invalid_data",
2761
+ NOT_FOUND: "not_found",
2762
+ INVALID_PATH: "invalid_path",
2763
+ INVALID_OPERATION: "invalid_operation",
2764
+ INTERNAL_ERROR: "internal_error",
2765
+ CONDITION_FAILED: "condition_failed",
2766
+ INVALID_QUERY: "invalid_query",
2767
+ AUTH_REQUIRED: "auth_required"
2768
+ };
2769
+ var DEFAULT_LARK_DOMAIN = "larkdb.net";
2770
+
2819
2771
  // src/OnDisconnect.ts
2820
2772
  var OnDisconnect = class {
2821
2773
  constructor(db, path) {
@@ -2919,6 +2871,125 @@ function generatePushId() {
2919
2871
  return id;
2920
2872
  }
2921
2873
 
2874
+ // src/utils/validation.ts
2875
+ var MAX_KEY_BYTES = 768;
2876
+ var INVALID_KEY_CHARS = /[.#$\[\]\/]/;
2877
+ var CONTROL_CHARS = /[\x00-\x1f\x7f]/;
2878
+ function hasControlChars(str) {
2879
+ return CONTROL_CHARS.test(str);
2880
+ }
2881
+ function getByteLength(str) {
2882
+ if (typeof TextEncoder !== "undefined") {
2883
+ return new TextEncoder().encode(str).length;
2884
+ }
2885
+ return Buffer.byteLength(str, "utf8");
2886
+ }
2887
+ function validateKey(key, context) {
2888
+ const prefix = context ? `${context}: ` : "";
2889
+ if (typeof key !== "string") {
2890
+ throw new LarkError(
2891
+ ErrorCode.INVALID_DATA,
2892
+ `${prefix}key must be a string`
2893
+ );
2894
+ }
2895
+ if (key.length === 0) {
2896
+ throw new LarkError(
2897
+ ErrorCode.INVALID_DATA,
2898
+ `${prefix}key cannot be empty`
2899
+ );
2900
+ }
2901
+ if (INVALID_KEY_CHARS.test(key)) {
2902
+ throw new LarkError(
2903
+ ErrorCode.INVALID_DATA,
2904
+ `${prefix}key "${key}" contains invalid characters (. $ # [ ] / are not allowed)`
2905
+ );
2906
+ }
2907
+ if (hasControlChars(key)) {
2908
+ throw new LarkError(
2909
+ ErrorCode.INVALID_DATA,
2910
+ `${prefix}key "${key}" contains control characters (ASCII 0-31, 127 are not allowed)`
2911
+ );
2912
+ }
2913
+ const byteLength = getByteLength(key);
2914
+ if (byteLength > MAX_KEY_BYTES) {
2915
+ throw new LarkError(
2916
+ ErrorCode.INVALID_DATA,
2917
+ `${prefix}key "${key.substring(0, 50)}..." exceeds maximum size of ${MAX_KEY_BYTES} bytes (got ${byteLength})`
2918
+ );
2919
+ }
2920
+ }
2921
+ function validatePath(path, context) {
2922
+ if (typeof path !== "string") {
2923
+ throw new LarkError(
2924
+ ErrorCode.INVALID_PATH,
2925
+ `${context ? `${context}: ` : ""}path must be a string`
2926
+ );
2927
+ }
2928
+ if (path === "" || path === "/") {
2929
+ return;
2930
+ }
2931
+ const segments = path.split("/").filter((s) => s.length > 0);
2932
+ for (const segment of segments) {
2933
+ if (segment === ".priority" || segment === ".value" || segment === ".sv" || segment === ".info") {
2934
+ continue;
2935
+ }
2936
+ validateKey(segment, context);
2937
+ }
2938
+ }
2939
+ function validateValue(value, context, path = "") {
2940
+ const prefix = context ? `${context}: ` : "";
2941
+ const location = path ? ` at "${path}"` : "";
2942
+ if (value === null || value === void 0) {
2943
+ return;
2944
+ }
2945
+ if (typeof value === "string") {
2946
+ if (hasControlChars(value)) {
2947
+ throw new LarkError(
2948
+ ErrorCode.INVALID_DATA,
2949
+ `${prefix}string value${location} contains control characters (ASCII 0-31, 127 are not allowed)`
2950
+ );
2951
+ }
2952
+ return;
2953
+ }
2954
+ if (typeof value === "number" || typeof value === "boolean") {
2955
+ return;
2956
+ }
2957
+ if (Array.isArray(value)) {
2958
+ for (let i = 0; i < value.length; i++) {
2959
+ validateValue(value[i], context, path ? `${path}/${i}` : String(i));
2960
+ }
2961
+ return;
2962
+ }
2963
+ if (typeof value === "object") {
2964
+ const obj = value;
2965
+ const keys = Object.keys(obj);
2966
+ if (".value" in obj) {
2967
+ const otherKeys = keys.filter((k) => k !== ".value" && k !== ".priority");
2968
+ if (otherKeys.length > 0) {
2969
+ const loc = path || "/";
2970
+ throw new LarkError(
2971
+ ErrorCode.INVALID_DATA,
2972
+ `${prefix}data at ${loc} contains ".value" alongside other children (${otherKeys.join(", ")}). ".value" can only be used with ".priority" for primitives with priority.`
2973
+ );
2974
+ }
2975
+ }
2976
+ for (const [key, childValue] of Object.entries(obj)) {
2977
+ if (key !== ".priority" && key !== ".value" && key !== ".sv") {
2978
+ validateKey(key, context);
2979
+ }
2980
+ validateValue(childValue, context, path ? `${path}/${key}` : key);
2981
+ }
2982
+ return;
2983
+ }
2984
+ throw new LarkError(
2985
+ ErrorCode.INVALID_DATA,
2986
+ `${prefix}invalid value type${location}: ${typeof value}`
2987
+ );
2988
+ }
2989
+ function validateWriteData(value, context) {
2990
+ validateValue(value, context);
2991
+ }
2992
+
2922
2993
  // src/DatabaseReference.ts
2923
2994
  function isInfoPath(path) {
2924
2995
  return path === "/.info" || path.startsWith("/.info/");
@@ -2994,7 +3065,6 @@ var DatabaseReference = class _DatabaseReference {
2994
3065
  * For queries (created via orderBy*, limitTo*, startAt, etc.), this returns
2995
3066
  * a reference to the same path without query constraints.
2996
3067
  * For non-query references, this returns the reference itself.
2997
- * This matches Firebase's Query.ref behavior.
2998
3068
  */
2999
3069
  get ref() {
3000
3070
  if (Object.keys(this._query).length === 0) {
@@ -3019,7 +3089,7 @@ var DatabaseReference = class _DatabaseReference {
3019
3089
  * Returns "default" for non-query references (no constraints).
3020
3090
  * Returns a sorted JSON string of wire-format params for queries.
3021
3091
  *
3022
- * This matches Firebase's queryIdentifier format for wire compatibility.
3092
+ * Used for wire protocol and subscription deduplication.
3023
3093
  */
3024
3094
  get queryIdentifier() {
3025
3095
  const queryObj = {};
@@ -3078,7 +3148,7 @@ var DatabaseReference = class _DatabaseReference {
3078
3148
  }
3079
3149
  /**
3080
3150
  * Get the data at this location. Alias for once('value').
3081
- * This is Firebase's newer API for reading data.
3151
+ * Alternative to once('value') for reading data.
3082
3152
  */
3083
3153
  async get() {
3084
3154
  return this.once("value");
@@ -3090,6 +3160,7 @@ var DatabaseReference = class _DatabaseReference {
3090
3160
  * Get a reference to a child path.
3091
3161
  */
3092
3162
  child(path) {
3163
+ validatePath(path, "child");
3093
3164
  const childPath = joinPath(this._path, path);
3094
3165
  return new _DatabaseReference(this._db, childPath);
3095
3166
  }
@@ -3106,6 +3177,7 @@ var DatabaseReference = class _DatabaseReference {
3106
3177
  */
3107
3178
  async set(value) {
3108
3179
  validateNotInfoPath(this._path, "set");
3180
+ validateWriteData(value, "set");
3109
3181
  if (this._db.isVolatilePath(this._path)) {
3110
3182
  this._db._sendVolatileSet(this._path, value);
3111
3183
  return;
@@ -3115,7 +3187,7 @@ var DatabaseReference = class _DatabaseReference {
3115
3187
  /**
3116
3188
  * Update specific children at this location without overwriting other children.
3117
3189
  *
3118
- * Also supports Firebase-style multi-path updates when keys look like paths
3190
+ * Also supports multi-path updates when keys look like paths
3119
3191
  * (start with '/'). In this mode, each path is written atomically as a transaction.
3120
3192
  *
3121
3193
  * @example
@@ -3135,6 +3207,16 @@ var DatabaseReference = class _DatabaseReference {
3135
3207
  */
3136
3208
  async update(values) {
3137
3209
  validateNotInfoPath(this._path, "update");
3210
+ for (const [key, value] of Object.entries(values)) {
3211
+ if (key.includes("/")) {
3212
+ validatePath(key, "update");
3213
+ } else {
3214
+ validateKey(key, "update");
3215
+ }
3216
+ if (value !== null) {
3217
+ validateWriteData(value, "update");
3218
+ }
3219
+ }
3138
3220
  const hasPathKeys = Object.keys(values).some((key) => key.startsWith("/"));
3139
3221
  if (hasPathKeys) {
3140
3222
  const ops = [];
@@ -3186,13 +3268,17 @@ var DatabaseReference = class _DatabaseReference {
3186
3268
  * For objects: injects `.priority` into the value object.
3187
3269
  * For primitives: wraps as `{ '.value': primitive, '.priority': priority }`.
3188
3270
  *
3189
- * This follows Firebase's wire format for primitives with priority.
3271
+ * Uses { '.value': value, '.priority': priority } format for the wire protocol.
3190
3272
  *
3191
3273
  * @param value - The value to write
3192
3274
  * @param priority - The priority for ordering
3193
3275
  */
3194
3276
  async setWithPriority(value, priority) {
3195
3277
  validateNotInfoPath(this._path, "setWithPriority");
3278
+ validateWriteData(value, "setWithPriority");
3279
+ if (typeof priority === "string") {
3280
+ validateWriteData(priority, "setWithPriority (priority)");
3281
+ }
3196
3282
  if (value === null || value === void 0) {
3197
3283
  await this._db._sendSet(this._path, value);
3198
3284
  return;
@@ -3734,17 +3820,11 @@ var DatabaseReference = class _DatabaseReference {
3734
3820
  }
3735
3821
  }
3736
3822
  /**
3737
- * Validate that a key is a valid Firebase key format.
3738
- * Invalid characters: . $ # [ ] /
3739
- * Also cannot start or end with .
3823
+ * Validate that a key is a valid key format.
3824
+ * Delegates to the shared validation utility.
3740
3825
  */
3741
3826
  _validateKeyFormat(methodName, key) {
3742
- if (/[.#$\[\]\/]/.test(key)) {
3743
- throw new LarkError(
3744
- ErrorCode.INVALID_QUERY,
3745
- `Query.${methodName}: invalid key "${key}" - keys cannot contain . # $ [ ] /`
3746
- );
3747
- }
3827
+ validateKey(key, `Query.${methodName}`);
3748
3828
  }
3749
3829
  // ============================================
3750
3830
  // Internal Helpers
@@ -3948,9 +4028,9 @@ var DataSnapshot = class _DataSnapshot {
3948
4028
  * Get a child snapshot at the specified path.
3949
4029
  *
3950
4030
  * Special handling:
3951
- * - `.priority` returns the priority value (Firebase compatible)
4031
+ * - `.priority` returns the priority value
3952
4032
  * - For wrapped primitives, only `.priority` returns data; other paths return null
3953
- * - Non-existent paths return a snapshot with val() === null (Firebase compatible)
4033
+ * - Non-existent paths return a snapshot with val() === null
3954
4034
  */
3955
4035
  child(path) {
3956
4036
  const childPath = joinPath(this._path, path);
@@ -4054,7 +4134,6 @@ var DataSnapshot = class _DataSnapshot {
4054
4134
  }
4055
4135
  /**
4056
4136
  * Check if this snapshot was from a volatile (high-frequency) update.
4057
- * This is a Lark extension not present in Firebase.
4058
4137
  */
4059
4138
  isVolatile() {
4060
4139
  return this._volatile;
@@ -4063,7 +4142,6 @@ var DataSnapshot = class _DataSnapshot {
4063
4142
  * Get the server timestamp for this snapshot (milliseconds since Unix epoch).
4064
4143
  * Only present on volatile value events. Use deltas between timestamps for
4065
4144
  * interpolation rather than absolute times to avoid clock sync issues.
4066
- * This is a Lark extension not present in Firebase.
4067
4145
  */
4068
4146
  getServerTimestamp() {
4069
4147
  return this._serverTimestamp;
@@ -4112,28 +4190,6 @@ function isVolatilePath(path, patterns) {
4112
4190
  }
4113
4191
 
4114
4192
  // src/LarkDatabase.ts
4115
- function validateWriteData(data, path = "") {
4116
- if (!data || typeof data !== "object" || Array.isArray(data)) {
4117
- return;
4118
- }
4119
- const obj = data;
4120
- const keys = Object.keys(obj);
4121
- if (".value" in obj) {
4122
- const otherKeys = keys.filter((k) => k !== ".value" && k !== ".priority");
4123
- if (otherKeys.length > 0) {
4124
- const location = path || "/";
4125
- throw new LarkError(
4126
- "invalid_data",
4127
- `Data at ${location} contains ".value" alongside other children (${otherKeys.join(", ")}). ".value" can only be used with ".priority" for primitives with priority.`
4128
- );
4129
- }
4130
- }
4131
- for (const key of keys) {
4132
- if (key !== ".priority" && key !== ".value") {
4133
- validateWriteData(obj[key], path ? `${path}/${key}` : `/${key}`);
4134
- }
4135
- }
4136
- }
4137
4193
  var RECONNECT_BASE_DELAY_MS = 1e3;
4138
4194
  var RECONNECT_MAX_DELAY_MS = 3e4;
4139
4195
  var RECONNECT_JITTER_FACTOR = 0.5;
@@ -4208,7 +4264,7 @@ var LarkDatabase = class {
4208
4264
  this._state = "disconnected";
4209
4265
  this._auth = null;
4210
4266
  this._databaseId = null;
4211
- this._coordinatorUrl = null;
4267
+ this._domain = null;
4212
4268
  this._volatilePaths = [];
4213
4269
  this._transportType = null;
4214
4270
  // Auth state
@@ -4271,8 +4327,9 @@ var LarkDatabase = class {
4271
4327
  * @internal Get the base URL for reference toString().
4272
4328
  */
4273
4329
  _getBaseUrl() {
4274
- if (this._coordinatorUrl && this._databaseId) {
4275
- return `${this._coordinatorUrl}/${this._databaseId}`;
4330
+ if (this._domain && this._databaseId) {
4331
+ const projectId = this._databaseId.split("/")[0];
4332
+ return `https://${projectId}.${this._domain}`;
4276
4333
  }
4277
4334
  return "lark://";
4278
4335
  }
@@ -4324,7 +4381,7 @@ var LarkDatabase = class {
4324
4381
  * Connect to a database.
4325
4382
  *
4326
4383
  * @param databaseId - Database ID in format "project/database"
4327
- * @param options - Connection options (token, anonymous, coordinator URL)
4384
+ * @param options - Connection options (token, anonymous, domain)
4328
4385
  */
4329
4386
  async connect(databaseId, options = {}) {
4330
4387
  if (this._state !== "disconnected") {
@@ -4348,19 +4405,17 @@ var LarkDatabase = class {
4348
4405
  const previousState = this._state;
4349
4406
  this._state = isReconnect ? "reconnecting" : "connecting";
4350
4407
  this._databaseId = databaseId;
4351
- this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
4408
+ this._domain = options.domain || DEFAULT_LARK_DOMAIN;
4352
4409
  if (!isReconnect) {
4353
4410
  this._currentToken = options.token || "";
4354
4411
  this._isAnonymous = !options.token && options.anonymous !== false;
4355
4412
  }
4356
4413
  try {
4357
- const coordinatorUrl = this._coordinatorUrl;
4358
- const coordinator = new Coordinator(coordinatorUrl);
4359
- const connectResponse = await coordinator.connect(databaseId, {
4360
- token: options.token,
4361
- anonymous: options.anonymous
4362
- });
4363
- const wsUrl = connectResponse.ws_url;
4414
+ const projectId = databaseId.split("/")[0];
4415
+ if (!projectId) {
4416
+ throw new Error('Invalid database ID: must be in format "projectId/databaseName"');
4417
+ }
4418
+ const wsUrl = `wss://${projectId}.${this._domain}/ws`;
4364
4419
  const transportResult = await createTransport(
4365
4420
  wsUrl,
4366
4421
  {
@@ -4521,7 +4576,7 @@ var LarkDatabase = class {
4521
4576
  this._auth = null;
4522
4577
  this._databaseId = null;
4523
4578
  this._volatilePaths = [];
4524
- this._coordinatorUrl = null;
4579
+ this._domain = null;
4525
4580
  this._connectionId = null;
4526
4581
  this._connectOptions = null;
4527
4582
  this._transportType = null;
@@ -4718,7 +4773,7 @@ var LarkDatabase = class {
4718
4773
  *
4719
4774
  * Supports two syntaxes:
4720
4775
  *
4721
- * **Object syntax** (like Firebase multi-path update):
4776
+ * **Object syntax** (multi-path update):
4722
4777
  * ```javascript
4723
4778
  * await db.transaction({
4724
4779
  * '/users/alice/name': 'Alice',
@@ -4756,14 +4811,20 @@ var LarkDatabase = class {
4756
4811
  */
4757
4812
  async convertToTxOp(op) {
4758
4813
  const path = normalizePath(op.path) || "/";
4814
+ validatePath(op.path, "transaction");
4759
4815
  switch (op.op) {
4760
4816
  case "set":
4817
+ validateWriteData(op.value, "transaction");
4761
4818
  return { o: "s", p: path, v: op.value };
4762
4819
  case "update":
4820
+ validateWriteData(op.value, "transaction");
4763
4821
  return { o: "u", p: path, v: op.value };
4764
4822
  case "delete":
4765
4823
  return { o: "d", p: path };
4766
4824
  case "condition":
4825
+ if (op.value !== void 0 && op.value !== null) {
4826
+ validateWriteData(op.value, "transaction condition");
4827
+ }
4767
4828
  if (isPrimitive(op.value)) {
4768
4829
  return { o: "c", p: path, v: op.value };
4769
4830
  } else {
@@ -4781,10 +4842,12 @@ var LarkDatabase = class {
4781
4842
  convertObjectToTxOps(obj) {
4782
4843
  const ops = [];
4783
4844
  for (const [path, value] of Object.entries(obj)) {
4845
+ validatePath(path, "transaction");
4784
4846
  const normalizedPath = normalizePath(path) || "/";
4785
4847
  if (value === null) {
4786
4848
  ops.push({ o: "d", p: normalizedPath });
4787
4849
  } else {
4850
+ validateWriteData(value, "transaction");
4788
4851
  ops.push({ o: "s", p: normalizedPath, v: value });
4789
4852
  }
4790
4853
  }