instant-cli 1.0.21-branch-cli-codex-update.25192174695.1 → 1.0.21-branch-cli-codex-update.25193076159.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > instant-cli@1.0.21-branch-cli-codex-update.25192174695.1 build /home/runner/work/instant/instant/client/packages/cli
2
+ > instant-cli@1.0.21-branch-cli-codex-update.25193076159.1 build /home/runner/work/instant/instant/client/packages/cli
3
3
  > rm -rf dist; tsc -p tsconfig.build.json
4
4
 
@@ -1,10 +1,9 @@
1
1
  import { Effect, Schema } from 'effect';
2
- import { FileSystem } from '@effect/platform';
3
2
  import { GlobalOpts } from '../../../context/globalOpts.ts';
4
3
  export declare const ClientTypeSchema: Schema.Literal<["google", "github", "apple", "linkedin", "clerk", "firebase"]>;
5
4
  export declare const authClientAddCmd: (opts: {
6
5
  type?: string | undefined;
7
6
  name?: string | undefined;
8
7
  app?: string | undefined;
9
- } & Record<string, unknown>) => Effect.Effect<void | undefined, import("../../../lib/ui.ts").UIError | import("../../../lib/http.ts").InstantHttpError | import("effect/Cause").TimeoutException | import("@effect/platform/HttpClientError").RequestError | import("effect/ParseResult").ParseError | import("@effect/platform/HttpClientError").ResponseError, GlobalOpts | FileSystem.FileSystem | import("../../../lib/http.ts").InstantHttpAuthed | import("../../../context/currentApp.ts").CurrentApp>;
8
+ } & Record<string, unknown>) => Effect.Effect<void | undefined, import("../../../lib/ui.ts").UIError | import("../../../lib/http.ts").InstantHttpError | import("effect/Cause").TimeoutException | import("@effect/platform/HttpClientError").RequestError | import("effect/ParseResult").ParseError | import("@effect/platform/HttpClientError").ResponseError, GlobalOpts | import("@effect/platform/FileSystem").FileSystem | import("../../../lib/http.ts").InstantHttpAuthed | import("../../../context/currentApp.ts").CurrentApp>;
10
9
  //# sourceMappingURL=add.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,EAAE,MAAM,QAAQ,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAiC5D,eAAO,MAAM,gBAAgB,gFAO5B,CAAC;AAq4BF,eAAO,MAAM,gBAAgB;;;;6eAwD5B,CAAC"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAiD5D,eAAO,MAAM,gBAAgB,gFAO5B,CAAC;AAosBF,eAAO,MAAM,gBAAgB;;;;wgBAwD5B,CAAC"}
@@ -1,17 +1,22 @@
1
1
  import { Effect, Match, Option, Schema } from 'effect';
2
- import { FileSystem } from '@effect/platform';
3
2
  import { BadArgsError } from "../../../errors.js";
4
3
  import { GlobalOpts } from "../../../context/globalOpts.js";
5
- import { optOrPrompt, optOrPromptBoolean, runUIEffect, stripFirstBlankLine, validateRequired, } from "../../../lib/ui.js";
4
+ import { optOrPrompt, optOrPromptBoolean, runUIEffect, validateRequired, } from "../../../lib/ui.js";
6
5
  import { addOAuthClient, findName, getClientNameAndProvider, getOrCreateProvider, } from "../../../lib/oauth.js";
7
6
  import { DEFAULT_OAUTH_CALLBACK_URL, GOOGLE_AUTHORIZATION_ENDPOINT, GOOGLE_DISCOVERY_ENDPOINT, GOOGLE_TOKEN_ENDPOINT, APPLE_AUTHORIZATION_ENDPOINT, APPLE_DISCOVERY_ENDPOINT, APPLE_TOKEN_ENDPOINT, LINKEDIN_AUTHORIZATION_ENDPOINT, LINKEDIN_DISCOVERY_ENDPOINT, LINKEDIN_TOKEN_ENDPOINT, } from '@instantdb/platform';
8
7
  import { UI } from "../../../ui/index.js";
9
8
  import chalk from 'chalk';
10
9
  import boxen from 'boxen';
11
10
  import { link } from "../../../logging.js";
12
- import { getFlag, hasAnyFlag, isTrueFlag } from "../../../util/flags.js";
11
+ import { appleKeyIdPrompt, applePrivateKeyFilePrompt, appleServicesIdPrompt, appleTeamIdPrompt, clerkPublishableKeyPrompt, clientIdPrompt, clientSecretPrompt, domainFromClerkKey, firebaseDiscoveryEndpoint, firebaseProjectIdPrompt, getFlag, hasAnyFlag, isTrueFlag, readPrivateKeyFile, redirectUriPrompt, validateFirebaseProjectId, } from "./shared.js";
13
12
  export const ClientTypeSchema = Schema.Literal('google', 'github', 'apple', 'linkedin', 'clerk', 'firebase');
14
13
  const GoogleAppTypeSchema = Schema.Literal('web', 'ios', 'android', 'button-for-web');
14
+ const googleConsoleUrl = 'https://console.developers.google.com/apis/credentials';
15
+ const githubDeveloperUrl = 'https://github.com/settings/developers';
16
+ const linkedinDeveloperUrl = 'https://www.linkedin.com/developers/apps';
17
+ const optionalRedirectPrompt = redirectUriPrompt({
18
+ heading: 'Custom redirect URI (optional):',
19
+ });
15
20
  const selectGoogleAppType = (value) => Effect.gen(function* () {
16
21
  const { yes } = yield* GlobalOpts;
17
22
  return yield* Option.fromNullable(value).pipe(Effect.catchTag('NoSuchElementException', () => {
@@ -164,14 +169,7 @@ const handleGoogleClient = Effect.fn(function* (opts) {
164
169
  required: !useSharedCredentials,
165
170
  skipIf: useSharedCredentials,
166
171
  skipMessage: '--client-id is not compatible with --dev-credentials. Drop one or the other.',
167
- prompt: {
168
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
169
- modifyOutput: UI.modifiers.piped([
170
- UI.modifiers.topPadding,
171
- UI.modifiers.dimOnComplete,
172
- ]),
173
- validate: validateRequired,
174
- },
172
+ prompt: clientIdPrompt({ providerUrl: googleConsoleUrl }),
175
173
  });
176
174
  const usesCustomWebCredentials = !useSharedCredentials && appType === 'web';
177
175
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
@@ -181,34 +179,11 @@ const handleGoogleClient = Effect.fn(function* (opts) {
181
179
  skipMessage: useSharedCredentials
182
180
  ? '--client-secret is not compatible with --dev-credentials. Drop one or the other.'
183
181
  : undefined,
184
- prompt: {
185
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
186
- validate: validateRequired,
187
- sensitive: true,
188
- modifyOutput: UI.modifiers.piped([
189
- UI.modifiers.topPadding,
190
- UI.modifiers.dimOnComplete,
191
- ]),
192
- },
182
+ prompt: clientSecretPrompt({ providerUrl: googleConsoleUrl }),
193
183
  });
194
184
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
195
185
  required: false,
196
- prompt: {
197
- prompt: '',
198
- placeholder: 'https://yoursite.com/oauth/callback',
199
- modifyOutput: UI.modifiers.piped([
200
- (output, status) => {
201
- if (status === 'idle') {
202
- return (`\nCustom redirect URI (optional):
203
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
204
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
205
- stripFirstBlankLine(output));
206
- }
207
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
208
- },
209
- UI.modifiers.dimOnComplete,
210
- ]),
211
- },
186
+ prompt: optionalRedirectPrompt,
212
187
  simpleName: '--custom-redirect-uri',
213
188
  skipIf: !usesCustomWebCredentials,
214
189
  skipMessage: useSharedCredentials
@@ -257,49 +232,19 @@ const handleGithubClient = Effect.fn(function* (opts) {
257
232
  simpleName: '--client-id',
258
233
  required: true,
259
234
  skipIf: false,
260
- prompt: {
261
- prompt: `Client ID ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
262
- modifyOutput: UI.modifiers.piped([
263
- UI.modifiers.topPadding,
264
- UI.modifiers.dimOnComplete,
265
- ]),
266
- validate: validateRequired,
267
- },
235
+ prompt: clientIdPrompt({ providerUrl: githubDeveloperUrl }),
268
236
  });
269
237
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
270
238
  required: true,
271
239
  skipIf: false,
272
240
  simpleName: '--client-secret',
273
- prompt: {
274
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
275
- validate: validateRequired,
276
- sensitive: true,
277
- modifyOutput: UI.modifiers.piped([
278
- UI.modifiers.topPadding,
279
- UI.modifiers.dimOnComplete,
280
- ]),
281
- },
241
+ prompt: clientSecretPrompt({ providerUrl: githubDeveloperUrl }),
282
242
  });
283
243
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
284
244
  required: false,
285
245
  simpleName: '--custom-redirect-uri',
286
246
  skipIf: false,
287
- prompt: {
288
- prompt: '',
289
- placeholder: 'https://yoursite.com/oauth/callback',
290
- modifyOutput: UI.modifiers.piped([
291
- (output, status) => {
292
- if (status === 'idle') {
293
- return (`\nCustom redirect URI (optional):
294
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
295
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
296
- stripFirstBlankLine(output));
297
- }
298
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
299
- },
300
- UI.modifiers.dimOnComplete,
301
- ]),
302
- },
247
+ prompt: optionalRedirectPrompt,
303
248
  });
304
249
  if (!clientName) {
305
250
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -335,49 +280,19 @@ const handleLinkedInClient = Effect.fn(function* (opts) {
335
280
  simpleName: '--client-id',
336
281
  required: true,
337
282
  skipIf: false,
338
- prompt: {
339
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
340
- modifyOutput: UI.modifiers.piped([
341
- UI.modifiers.topPadding,
342
- UI.modifiers.dimOnComplete,
343
- ]),
344
- validate: validateRequired,
345
- },
283
+ prompt: clientIdPrompt({ providerUrl: linkedinDeveloperUrl }),
346
284
  });
347
285
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
348
286
  required: true,
349
287
  skipIf: false,
350
288
  simpleName: '--client-secret',
351
- prompt: {
352
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
353
- validate: validateRequired,
354
- sensitive: true,
355
- modifyOutput: UI.modifiers.piped([
356
- UI.modifiers.topPadding,
357
- UI.modifiers.dimOnComplete,
358
- ]),
359
- },
289
+ prompt: clientSecretPrompt({ providerUrl: linkedinDeveloperUrl }),
360
290
  });
361
291
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
362
292
  required: false,
363
293
  simpleName: '--custom-redirect-uri',
364
294
  skipIf: false,
365
- prompt: {
366
- prompt: '',
367
- placeholder: 'https://yoursite.com/oauth/callback',
368
- modifyOutput: UI.modifiers.piped([
369
- (output, status) => {
370
- if (status === 'idle') {
371
- return (`\nCustom redirect URI (optional):
372
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
373
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
374
- stripFirstBlankLine(output));
375
- }
376
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
377
- },
378
- UI.modifiers.dimOnComplete,
379
- ]),
380
- },
295
+ prompt: optionalRedirectPrompt,
381
296
  });
382
297
  if (!clientName) {
383
298
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -407,22 +322,6 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
407
322
  ...redirectMessages,
408
323
  ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
409
324
  });
410
- const readPrivateKeyFile = Effect.fn('readPrivateKeyFile')(function* (path) {
411
- const fs = yield* FileSystem.FileSystem;
412
- // Strip shell-escape backslashes so paths like "file\ (2).p8" resolve correctly.
413
- // Only on POSIX — Windows uses backslashes as path separators.
414
- const normalizedPath = process.platform === 'win32' ? path : path.replace(/\\(.)/g, '$1');
415
- const contents = yield* fs.readFileString(normalizedPath, 'utf8').pipe(Effect.mapError((e) => new BadArgsError({
416
- message: `Could not read private key file at ${normalizedPath}: ${e.message}`,
417
- })));
418
- const trimmed = contents.trim();
419
- if (!trimmed) {
420
- return yield* BadArgsError.make({
421
- message: `Private key file at ${normalizedPath} is empty.`,
422
- });
423
- }
424
- return trimmed;
425
- });
426
325
  const handleAppleClient = Effect.fn(function* (opts) {
427
326
  const { yes } = yield* GlobalOpts;
428
327
  const { clientName, provider } = yield* getClientNameAndProvider('apple', opts);
@@ -430,14 +329,7 @@ const handleAppleClient = Effect.fn(function* (opts) {
430
329
  simpleName: '--services-id',
431
330
  required: true,
432
331
  skipIf: false,
433
- prompt: {
434
- prompt: `Services ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/identifiers/list/serviceId')})`)}`,
435
- modifyOutput: UI.modifiers.piped([
436
- UI.modifiers.topPadding,
437
- UI.modifiers.dimOnComplete,
438
- ]),
439
- validate: validateRequired,
440
- },
332
+ prompt: appleServicesIdPrompt({}),
441
333
  });
442
334
  // If any web-flow flag is provided, enable web flow; otherwise ask
443
335
  // (non-interactively with --yes we default to native-only).
@@ -463,42 +355,21 @@ const handleAppleClient = Effect.fn(function* (opts) {
463
355
  required: true,
464
356
  skipIf: skipWeb,
465
357
  skipMessage: `--team-id ${webSkipMessage}`,
466
- prompt: {
467
- prompt: `Team ID ${chalk.dim(`(from ${link('https://developer.apple.com/account#MembershipDetailsCard')})`)}`,
468
- validate: validateRequired,
469
- modifyOutput: UI.modifiers.piped([
470
- UI.modifiers.topPadding,
471
- UI.modifiers.dimOnComplete,
472
- ]),
473
- },
358
+ prompt: appleTeamIdPrompt({}),
474
359
  });
475
360
  const keyId = yield* optOrPrompt(opts['key-id'], {
476
361
  simpleName: '--key-id',
477
362
  required: true,
478
363
  skipIf: skipWeb,
479
364
  skipMessage: `--key-id ${webSkipMessage}`,
480
- prompt: {
481
- prompt: `Key ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/authkeys/list')})`)}`,
482
- validate: validateRequired,
483
- modifyOutput: UI.modifiers.piped([
484
- UI.modifiers.topPadding,
485
- UI.modifiers.dimOnComplete,
486
- ]),
487
- },
365
+ prompt: appleKeyIdPrompt({}),
488
366
  });
489
367
  const privateKeyPath = yield* optOrPrompt(opts['private-key-file'], {
490
368
  simpleName: '--private-key-file',
491
369
  required: true,
492
370
  skipIf: skipWeb,
493
371
  skipMessage: `--private-key-file ${webSkipMessage}`,
494
- prompt: {
495
- prompt: `Path to .p8 private key file ${chalk.dim('(downloaded from Apple)')}`,
496
- validate: validateRequired,
497
- modifyOutput: UI.modifiers.piped([
498
- UI.modifiers.topPadding,
499
- UI.modifiers.dimOnComplete,
500
- ]),
501
- },
372
+ prompt: applePrivateKeyFilePrompt({}),
502
373
  });
503
374
  const privateKey = privateKeyPath
504
375
  ? yield* readPrivateKeyFile(privateKeyPath)
@@ -508,22 +379,7 @@ const handleAppleClient = Effect.fn(function* (opts) {
508
379
  simpleName: '--custom-redirect-uri',
509
380
  skipIf: skipWeb,
510
381
  skipMessage: `--custom-redirect-uri ${webSkipMessage}`,
511
- prompt: {
512
- prompt: '',
513
- placeholder: 'https://yoursite.com/oauth/callback',
514
- modifyOutput: UI.modifiers.piped([
515
- (output, status) => {
516
- if (status === 'idle') {
517
- return (`\nCustom redirect URI (optional):
518
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
519
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
520
- stripFirstBlankLine(output));
521
- }
522
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
523
- },
524
- UI.modifiers.dimOnComplete,
525
- ]),
526
- },
382
+ prompt: optionalRedirectPrompt,
527
383
  });
528
384
  if (!clientName) {
529
385
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -572,22 +428,7 @@ const handleClerkClient = Effect.fn(function* (opts) {
572
428
  simpleName: '--publishable-key',
573
429
  required: true,
574
430
  skipIf: false,
575
- prompt: {
576
- prompt: `Clerk publishable key ${chalk.dim(`(from ${link('https://dashboard.clerk.com/last-active?path=api-keys')})`)}`,
577
- placeholder: 'pk_********************************************************',
578
- modifyOutput: UI.modifiers.piped([
579
- UI.modifiers.topPadding,
580
- UI.modifiers.dimOnComplete,
581
- ]),
582
- validate: (val) => {
583
- if (!val) {
584
- return 'Publishable key is required';
585
- }
586
- if (!val.startsWith('pk_')) {
587
- return 'Invalid publishable key. It should start with "pk_".';
588
- }
589
- },
590
- },
431
+ prompt: clerkPublishableKeyPrompt({}),
591
432
  });
592
433
  if (!clientName) {
593
434
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -624,27 +465,11 @@ const handleClerkClient = Effect.fn(function* (opts) {
624
465
  });
625
466
  const handleFirebaseClient = Effect.fn(function* (opts) {
626
467
  const { clientName, provider } = yield* getClientNameAndProvider('firebase', opts);
627
- const firebaseProjectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
628
468
  const projectId = yield* optOrPrompt(opts['project-id'], {
629
469
  simpleName: '--project-id',
630
470
  required: true,
631
471
  skipIf: false,
632
- prompt: {
633
- prompt: `Firebase project ID: (From Project Settings page on ${link('https://console.firebase.google.com/')})`,
634
- placeholder: '',
635
- modifyOutput: UI.modifiers.piped([
636
- UI.modifiers.topPadding,
637
- UI.modifiers.dimOnComplete,
638
- ]),
639
- validate: (val) => {
640
- if (!val) {
641
- return 'Project ID is required';
642
- }
643
- if (!firebaseProjectIdRegex.test(val)) {
644
- return 'Invalid Firebase project ID. It must be 6-30 characters, start with a lowercase letter, contain only lowercase letters, digits, and hyphens, and not end with a hyphen.';
645
- }
646
- },
647
- },
472
+ prompt: firebaseProjectIdPrompt({}),
648
473
  });
649
474
  // typeguard
650
475
  if (!clientName || !projectId) {
@@ -652,15 +477,14 @@ const handleFirebaseClient = Effect.fn(function* (opts) {
652
477
  message: 'Missing required arguments',
653
478
  });
654
479
  }
655
- if (!firebaseProjectIdRegex.test(projectId)) {
656
- return yield* BadArgsError.make({
657
- message: 'Invalid Firebase project ID. It must be 6-30 characters, start with a lowercase letter, contain only lowercase letters, digits, and hyphens, and not end with a hyphen.',
658
- });
480
+ const validationError = validateFirebaseProjectId(projectId);
481
+ if (validationError) {
482
+ return yield* BadArgsError.make({ message: validationError });
659
483
  }
660
484
  const response = yield* addOAuthClient({
661
485
  providerId: provider.id,
662
486
  clientName,
663
- discoveryEndpoint: `https://securetoken.google.com/${encodeURIComponent(projectId)}/.well-known/openid-configuration`,
487
+ discoveryEndpoint: firebaseDiscoveryEndpoint(projectId),
664
488
  });
665
489
  yield* Effect.log(boxen([
666
490
  `Firebase OAuth client created: ${response.client.client_name}`,
@@ -694,29 +518,4 @@ export const authClientAddCmd = Effect.fn(function* (opts) {
694
518
  yield* Effect.logError(e.message);
695
519
  yield* Effect.log(chalk.dim('hint: run `instant-cli auth client add --help` for the list of available arguments'));
696
520
  })));
697
- function domainFromClerkKey(key) {
698
- try {
699
- const parts = key.split('_');
700
- const domainPartB64 = parts[parts.length - 1];
701
- const domainPart = base64Decode(domainPartB64);
702
- return domainPart.replace('$', '');
703
- }
704
- catch (e) {
705
- console.error('Error getting domain from clerk key', e);
706
- return null;
707
- }
708
- }
709
- // Base64 decode, switching to url-safe decode if we hit an error
710
- // Can't be sure which method Clerk uses because you can't generate
711
- // `+` or `/` with characters that go in a normal host. Urls with
712
- // chinese characters exist, they might encode to `+` or `/`, and
713
- // Clerk might support them, so we'll be safe and do both.
714
- function base64Decode(s) {
715
- try {
716
- return Buffer.from(s, 'base64').toString('utf-8');
717
- }
718
- catch (e) {
719
- return Buffer.from(s, 'base64url').toString('utf-8');
720
- }
721
- }
722
521
  //# sourceMappingURL=add.js.map