@algolia/wizard 0.115.0 → 0.117.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/main.js CHANGED
@@ -1245,7 +1245,7 @@ function SelectPrompt({
1245
1245
  helpText,
1246
1246
  messages,
1247
1247
  table,
1248
- list,
1248
+ list: list2,
1249
1249
  error,
1250
1250
  multi,
1251
1251
  cancelable,
@@ -1334,7 +1334,7 @@ function SelectPrompt({
1334
1334
  error && /* @__PURE__ */ jsx8(Text10, { color: COLORS.danger, children: error }),
1335
1335
  messages?.map((m, i) => /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: m }, `msg-${i}`)),
1336
1336
  table && /* @__PURE__ */ jsx8(Table, { columns: table.columns, rows: table.rows }),
1337
- list && list.length > 0 && /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", children: list.map((item, i) => /* @__PURE__ */ jsxs9(Text10, { children: [
1337
+ list2 && list2.length > 0 && /* @__PURE__ */ jsx8(Box9, { flexDirection: "column", children: list2.map((item, i) => /* @__PURE__ */ jsxs9(Text10, { children: [
1338
1338
  /* @__PURE__ */ jsx8(
1339
1339
  Text10,
1340
1340
  {
@@ -1566,7 +1566,7 @@ function PromptInput() {
1566
1566
  }
1567
1567
 
1568
1568
  // src/workflows/default.ts
1569
- import { z as z30 } from "zod";
1569
+ import { z as z31 } from "zod";
1570
1570
 
1571
1571
  // src/core/orchestrator.ts
1572
1572
  import "zod";
@@ -2504,7 +2504,7 @@ async function ensureApplication(resumeAppId) {
2504
2504
  // package.json
2505
2505
  var package_default = {
2506
2506
  name: "@algolia/wizard",
2507
- version: "0.115.0",
2507
+ version: "0.117.0",
2508
2508
  description: "Magically implement Algolia functionality in your codebase",
2509
2509
  type: "module",
2510
2510
  engines: {
@@ -6421,9 +6421,9 @@ Output:
6421
6421
  ${JSON.stringify(s.output, null, 2)}`
6422
6422
  ).join("\n\n");
6423
6423
  }
6424
- async function runReviewAgent(request) {
6424
+ async function runReviewAgent(request2) {
6425
6425
  try {
6426
- return await runAgent(request);
6426
+ return await runAgent(request2);
6427
6427
  } catch (err) {
6428
6428
  if (!(err instanceof AgentReportedFailureError)) throw err;
6429
6429
  const parsed = reviewSchema.safeParse(err.output);
@@ -6461,7 +6461,7 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
6461
6461
  };
6462
6462
 
6463
6463
  // src/actions/implement.ts
6464
- import z29 from "zod";
6464
+ import z30 from "zod";
6465
6465
  import { access, mkdir as mkdir5, readdir as readdir3, readFile as readFile10 } from "node:fs/promises";
6466
6466
  import {
6467
6467
  basename as basename6,
@@ -6564,6 +6564,271 @@ function copyToClipboard(text, stream = process.stdout) {
6564
6564
  stream.write(`\x1B]52;c;${encoded}\x07`);
6565
6565
  }
6566
6566
 
6567
+ // src/lib/publicSearchSettings.ts
6568
+ var SETTINGS_TIMEOUT_MS = 15e3;
6569
+ var WRAPPER = /^(unordered|searchable|filterOnly|afterDistinct)\((.*)\)$/;
6570
+ function attributeKey(value) {
6571
+ return attributeKeys(value)[0] ?? "";
6572
+ }
6573
+ function attributeKeys(value) {
6574
+ const wrapped = WRAPPER.exec(value.trim());
6575
+ const inner = (wrapped?.[2] ?? value).trim();
6576
+ return inner.split(",").map((part) => part.trim().split(":")[0]?.trim()).filter((part) => Boolean(part));
6577
+ }
6578
+ function settingsThatHideAttributes(current, hide, searchableFallback) {
6579
+ if (hide.length === 0) return null;
6580
+ const hidden = unique(hide);
6581
+ const safeSearchableFallback = filterSettingsValues(
6582
+ searchableFallback,
6583
+ hidden
6584
+ );
6585
+ const drop = (value) => hasHiddenAttribute(value, hidden);
6586
+ const patch = {
6587
+ unretrievableAttributes: unique([
6588
+ ...(current.unretrievableAttributes ?? []).filter((value) => !drop(value)),
6589
+ ...hide
6590
+ ])
6591
+ };
6592
+ const currentSearchable = list(current.searchableAttributes);
6593
+ const filteredCurrent = currentSearchable ? filterSettingsValues(currentSearchable, hidden) : void 0;
6594
+ patch.searchableAttributes = filteredCurrent && filteredCurrent.length > 0 ? filteredCurrent : safeSearchableFallback;
6595
+ if (patch.searchableAttributes.length === 0) {
6596
+ throw new Error(
6597
+ `Cannot make public search safe: no searchable attributes remain after hiding ${hidden.join(", ")}.`
6598
+ );
6599
+ }
6600
+ const facets = list(current.attributesForFaceting);
6601
+ if (facets) patch.attributesForFaceting = filterSettingsValues(facets, hidden);
6602
+ const highlight = list(current.attributesToHighlight);
6603
+ if (highlight) {
6604
+ patch.attributesToHighlight = filterSettingsValues(highlight, hidden);
6605
+ }
6606
+ const snippet = list(current.attributesToSnippet);
6607
+ if (snippet) {
6608
+ patch.attributesToSnippet = filterSettingsValues(snippet, hidden);
6609
+ }
6610
+ return patch;
6611
+ }
6612
+ async function hideAttributesFromPublicSearch(args) {
6613
+ const patch = settingsThatHideAttributes(
6614
+ await readSettings(args),
6615
+ args.hide,
6616
+ args.searchableFallback
6617
+ );
6618
+ if (!patch) return;
6619
+ await writeSettings(args, patch);
6620
+ }
6621
+ async function readSettings(args) {
6622
+ const response = await request(args, "GET");
6623
+ if (response.status === 404 && args.createIfMissing) return {};
6624
+ assertOk(response, args.apiKey);
6625
+ const body = await response.json();
6626
+ if (!body || typeof body !== "object") return {};
6627
+ const record = body;
6628
+ return {
6629
+ searchableAttributes: record.searchableAttributes,
6630
+ attributesForFaceting: record.attributesForFaceting,
6631
+ unretrievableAttributes: record.unretrievableAttributes,
6632
+ attributesToHighlight: record.attributesToHighlight,
6633
+ attributesToSnippet: record.attributesToSnippet
6634
+ };
6635
+ }
6636
+ async function writeSettings(args, patch) {
6637
+ const response = await request(args, "PUT", patch);
6638
+ assertOk(response, args.apiKey);
6639
+ }
6640
+ async function request(args, method, body) {
6641
+ const fetchImpl = args.fetch ?? fetch;
6642
+ try {
6643
+ return await fetchImpl(settingsUrl(args.appId, args.indexName), {
6644
+ method,
6645
+ headers: {
6646
+ "X-Algolia-Application-Id": args.appId,
6647
+ "X-Algolia-API-Key": args.apiKey,
6648
+ "Content-Type": "application/json"
6649
+ },
6650
+ ...body ? { body: JSON.stringify(body) } : {},
6651
+ signal: AbortSignal.timeout(SETTINGS_TIMEOUT_MS)
6652
+ });
6653
+ } catch (err) {
6654
+ throw settingsError(err.message, args.apiKey);
6655
+ }
6656
+ }
6657
+ function assertOk(response, apiKey) {
6658
+ if (!response.ok) throw settingsError(String(response.status), apiKey);
6659
+ }
6660
+ function settingsError(detail, apiKey) {
6661
+ return new Error(
6662
+ `Algolia index settings request failed (${detail.replaceAll(apiKey, "***")})`
6663
+ );
6664
+ }
6665
+ function settingsUrl(appId, indexName) {
6666
+ return `https://${appId}.algolia.net/1/indexes/${encodeURIComponent(indexName)}/settings`;
6667
+ }
6668
+ function list(value) {
6669
+ if (!value || value.length === 0) return void 0;
6670
+ return value;
6671
+ }
6672
+ function unique(values) {
6673
+ return [...new Set(values)];
6674
+ }
6675
+ function filterSettingsValues(values, hidden) {
6676
+ return values.map((value) => filterSettingsValue(value, hidden)).filter((value) => value !== void 0);
6677
+ }
6678
+ function filterSettingsValue(value, hidden) {
6679
+ const trimmed = value.trim();
6680
+ const wrapped = WRAPPER.exec(trimmed);
6681
+ const wrapper = wrapped?.[1];
6682
+ const inner = (wrapped?.[2] ?? trimmed).trim();
6683
+ const parts = inner.split(",").map((part) => part.trim()).filter(Boolean);
6684
+ const kept = parts.filter(
6685
+ (part) => !attributeIsHidden(attributeKey(part), hidden)
6686
+ );
6687
+ if (kept.length === 0) return void 0;
6688
+ if (kept.length === parts.length) return value;
6689
+ return wrapper ? `${wrapper}(${kept.join(",")})` : kept.join(",");
6690
+ }
6691
+ function hasHiddenAttribute(value, hidden) {
6692
+ return attributeKeys(value).some((key) => attributeIsHidden(key, hidden));
6693
+ }
6694
+ function attributeIsHidden(key, hidden) {
6695
+ return hidden.some(
6696
+ (hiddenKey) => key === hiddenKey || key.startsWith(`${hiddenKey}.`) || hiddenKey.startsWith(`${key}.`)
6697
+ );
6698
+ }
6699
+
6700
+ // src/lib/sensitiveAttributes.ts
6701
+ import { z as z29 } from "zod";
6702
+ var TOKEN_SPLIT = /[^a-z0-9]+/;
6703
+ var recordExposureSchema = z29.object({
6704
+ omittedAttributes: z29.array(z29.string()),
6705
+ hiddenAttributes: z29.array(z29.string()),
6706
+ publicSearchSafe: z29.boolean()
6707
+ });
6708
+ function publicSearchKeyIsSafe(exposure) {
6709
+ return exposure?.publicSearchSafe === true;
6710
+ }
6711
+ function classifyAttributes(names) {
6712
+ const blocked = [];
6713
+ const personal = [];
6714
+ const seen = /* @__PURE__ */ new Set();
6715
+ for (const name of names) {
6716
+ if (seen.has(name)) continue;
6717
+ seen.add(name);
6718
+ const kind = classifyAttribute(name);
6719
+ if (kind === "blocked") blocked.push(name);
6720
+ else if (kind === "personal") personal.push(name);
6721
+ }
6722
+ return { blocked, personal };
6723
+ }
6724
+ function withheldAttributes(exposure) {
6725
+ if (!exposure) return [];
6726
+ return [
6727
+ .../* @__PURE__ */ new Set([...exposure.omittedAttributes, ...exposure.hiddenAttributes])
6728
+ ];
6729
+ }
6730
+ function visibleAttributes(attributes, exposure) {
6731
+ if (!exposure) return [...attributes];
6732
+ const drop = new Set(withheldAttributes(exposure));
6733
+ return attributes.filter((name) => !drop.has(name));
6734
+ }
6735
+ function ingestionExposureInstructions(exposure) {
6736
+ if (!exposure) return [];
6737
+ const lines = [];
6738
+ if (exposure.omittedAttributes.length > 0) {
6739
+ lines.push(
6740
+ `Do not write these attributes into records, sample data, or index settings: ${exposure.omittedAttributes.join(", ")}. Omit them even if the source has them.`
6741
+ );
6742
+ }
6743
+ if (exposure.hiddenAttributes.length > 0) {
6744
+ lines.push(
6745
+ `The user chose to index these personal attributes: ${exposure.hiddenAttributes.join(", ")}. Write them on the records, but do not add them to searchableAttributes, attributesForFaceting, attributesToHighlight, or attributesToSnippet. Wizard applies the public-search settings that hide them from browser search.`
6746
+ );
6747
+ }
6748
+ return lines;
6749
+ }
6750
+ function searchExposureInstructions(exposure) {
6751
+ const withheld = withheldAttributes(exposure);
6752
+ if (!exposure || withheld.length === 0) return [];
6753
+ const lines = [
6754
+ `Do not display, highlight, snippet, filter, or facet these attributes, even if a source file mentions them: ${withheld.join(", ")}.`
6755
+ ];
6756
+ if (exposure.hiddenAttributes.length > 0) {
6757
+ const names = exposure.hiddenAttributes.join(", ");
6758
+ lines.push(
6759
+ exposure.publicSearchSafe ? `These personal attributes may be stored on records, but a public hit does not include them: ${names}.` : `These personal attributes may be stored on records, and Wizard could not confirm that public search hides them: ${names}. Do not edit the ingestion script, the records, or the index settings to change that; it is outside the search step.`
6760
+ );
6761
+ }
6762
+ return lines;
6763
+ }
6764
+ function classifyAttribute(name) {
6765
+ const tokens = attributeTokens(name);
6766
+ if (tokens.length === 0) return null;
6767
+ if (isBlocked(tokens)) return "blocked";
6768
+ if (isPersonal(tokens)) return "personal";
6769
+ return null;
6770
+ }
6771
+ function attributeTokens(name) {
6772
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase();
6773
+ return spaced.split(TOKEN_SPLIT).filter(Boolean);
6774
+ }
6775
+ function has(tokens, ...options) {
6776
+ return options.some((option) => tokens.includes(option));
6777
+ }
6778
+ function hasAll(tokens, ...need) {
6779
+ return need.every((token) => tokens.includes(token));
6780
+ }
6781
+ function isBlocked(tokens) {
6782
+ if (has(tokens, "ssn", "itin", "cvv", "cvc", "passport")) return true;
6783
+ if (has(tokens, "password", "secret", "token", "iban")) return true;
6784
+ if (has(tokens, "api", "private") && has(tokens, "key", "keys")) return true;
6785
+ if (hasAll(tokens, "account", "number")) return true;
6786
+ if (hasAll(tokens, "social", "security")) return true;
6787
+ if (hasAll(tokens, "social", "insurance")) return true;
6788
+ if (tokens.length === 1 && tokens[0] === "sin") return true;
6789
+ if (hasAll(tokens, "sin", "number")) return true;
6790
+ if (hasAll(tokens, "national", "id") || hasAll(tokens, "government", "id")) {
6791
+ return true;
6792
+ }
6793
+ if (has(tokens, "driver", "drivers", "driving") && has(tokens, "license", "licence")) {
6794
+ return true;
6795
+ }
6796
+ if (hasAll(tokens, "tax", "id") || hasAll(tokens, "taxpayer", "id")) {
6797
+ return true;
6798
+ }
6799
+ if (has(tokens, "tin") && (tokens.length === 1 || has(tokens, "number", "tax"))) {
6800
+ return true;
6801
+ }
6802
+ if (hasAll(tokens, "credit", "card") || hasAll(tokens, "card", "number")) {
6803
+ return true;
6804
+ }
6805
+ return false;
6806
+ }
6807
+ function isPersonal(tokens) {
6808
+ if (has(tokens, "email", "emails")) return true;
6809
+ if (hasAll(tokens, "e", "mail")) return true;
6810
+ if (has(tokens, "phone", "phones", "telephone", "tel")) return true;
6811
+ if (hasAll(tokens, "mobile", "phone") || hasAll(tokens, "mobile", "number")) {
6812
+ return true;
6813
+ }
6814
+ if (has(tokens, "stripe") && !has(tokens, "price", "product", "plan", "sku")) {
6815
+ return true;
6816
+ }
6817
+ if (["address", "addresses"].includes(tokens.at(-1) ?? "") || tokens.length === 1 && tokens[0] === "zip" || has(tokens, "street", "postcode", "zipcode") || hasAll(tokens, "address", "line") || hasAll(tokens, "address", "city") || hasAll(tokens, "address", "postal")) {
6818
+ return true;
6819
+ }
6820
+ if (hasAll(tokens, "postal", "code") || hasAll(tokens, "zip", "code")) {
6821
+ return true;
6822
+ }
6823
+ if (has(tokens, "dob", "birthday", "birthdate")) return true;
6824
+ if (hasAll(tokens, "birth", "date")) return true;
6825
+ if (has(tokens, "ip", "ipaddress")) return true;
6826
+ if (has(tokens, "lat", "lng", "latitude", "longitude") || hasAll(tokens, "geo", "point")) {
6827
+ return true;
6828
+ }
6829
+ return false;
6830
+ }
6831
+
6567
6832
  // src/lib/algoliaDocs.ts
6568
6833
  import { readFileSync, readdirSync, existsSync } from "node:fs";
6569
6834
  import { dirname as dirname4, join as join10 } from "node:path";
@@ -6623,51 +6888,52 @@ function getFrameworkSpecificDoc(frameworks) {
6623
6888
  }
6624
6889
 
6625
6890
  // src/actions/implement.ts
6626
- var implementSchema = z29.object({
6627
- summary: z29.string(),
6628
- ingestCommand: z29.string().optional(),
6629
- ingestScriptRan: z29.boolean().optional(),
6630
- ingestRecordCount: z29.number().optional(),
6631
- ingestDurationMs: z29.number().optional(),
6632
- ingestionSource: z29.enum(["local", "fileUpload", "generated"]),
6633
- searchConfig: z29.object({
6634
- filePath: z29.string().optional(),
6635
- vars: z29.array(
6636
- z29.object({
6637
- name: z29.string(),
6638
- value: z29.string()
6891
+ var implementSchema = z30.object({
6892
+ summary: z30.string(),
6893
+ ingestCommand: z30.string().optional(),
6894
+ ingestScriptRan: z30.boolean().optional(),
6895
+ ingestRecordCount: z30.number().optional(),
6896
+ ingestDurationMs: z30.number().optional(),
6897
+ ingestionSource: z30.enum(["local", "fileUpload", "generated"]),
6898
+ recordExposure: recordExposureSchema.optional(),
6899
+ searchConfig: z30.object({
6900
+ filePath: z30.string().optional(),
6901
+ vars: z30.array(
6902
+ z30.object({
6903
+ name: z30.string(),
6904
+ value: z30.string()
6639
6905
  })
6640
6906
  )
6641
6907
  }).optional(),
6642
- uiReport: z29.object({
6643
- files: z29.array(
6644
- z29.object({
6645
- label: z29.string(),
6646
- path: z29.string()
6908
+ uiReport: z30.object({
6909
+ files: z30.array(
6910
+ z30.object({
6911
+ label: z30.string(),
6912
+ path: z30.string()
6647
6913
  })
6648
6914
  ),
6649
- checkPassed: z29.boolean(),
6650
- typecheckCommand: z29.string().optional(),
6651
- devCommand: z29.string().optional()
6915
+ checkPassed: z30.boolean(),
6916
+ typecheckCommand: z30.string().optional(),
6917
+ devCommand: z30.string().optional()
6652
6918
  }).optional()
6653
6919
  });
6654
- var implementationOutputSchema = z29.object({
6655
- summary: z29.string(),
6656
- ingestCommand: z29.string().optional(),
6920
+ var implementationOutputSchema = z30.object({
6921
+ summary: z30.string(),
6922
+ ingestCommand: z30.string().optional(),
6657
6923
  // Only for the search use case: the path of whatever module the agent
6658
6924
  // defined the Algolia config constants in, so the wizard can check it
6659
6925
  // won't end up gitignored (it's public, meant to be committed).
6660
- searchConfigFile: z29.string().optional(),
6661
- uiFiles: z29.array(z29.object({ label: z29.string(), path: z29.string() })).optional(),
6662
- devCommand: z29.string().optional()
6926
+ searchConfigFile: z30.string().optional(),
6927
+ uiFiles: z30.array(z30.object({ label: z30.string(), path: z30.string() })).optional(),
6928
+ devCommand: z30.string().optional()
6663
6929
  });
6664
- var validationOutputSchema = z29.object({
6665
- summary: z29.string(),
6666
- sufficient: z29.boolean(),
6667
- additionalInstructions: z29.string().optional(),
6668
- unrelatedFailure: z29.string().optional(),
6669
- styleOnly: z29.boolean().optional(),
6670
- typecheckCommand: z29.string().optional()
6930
+ var validationOutputSchema = z30.object({
6931
+ summary: z30.string(),
6932
+ sufficient: z30.boolean(),
6933
+ additionalInstructions: z30.string().optional(),
6934
+ unrelatedFailure: z30.string().optional(),
6935
+ styleOnly: z30.boolean().optional(),
6936
+ typecheckCommand: z30.string().optional()
6671
6937
  });
6672
6938
  var MAX_IMPLEMENT_VALIDATION_ATTEMPTS = 3;
6673
6939
  var INGESTION_MAX_STEPS = 96;
@@ -6689,6 +6955,16 @@ var INGEST_RETRY = {
6689
6955
  skip: "Continue without records"
6690
6956
  };
6691
6957
  var INGEST_AGENT_RETRY_PROMPT = "Retry to let the wizard write and run the ingestion script again.";
6958
+ var PERSONAL_DATA_PROMPT = "Index this personal data?";
6959
+ var PERSONAL_DATA = {
6960
+ omit: "No, do not index personal data",
6961
+ ingest: "Yes, index personal data, but hide it from public search"
6962
+ };
6963
+ var HIDE_PERSONAL_DATA_RETRY_PROMPT = "Fix the cause (for example, check that the write key can edit index settings), then retry.";
6964
+ var HIDE_PERSONAL_DATA_RETRY = {
6965
+ retry: "Retry hiding personal data",
6966
+ skip: "Continue with a placeholder search key"
6967
+ };
6692
6968
  var FILE_UPLOAD_PATH_PROMPT = "Enter the path to your JSON, CSV, or TSV file (relative to the project root, or absolute):";
6693
6969
  var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
6694
6970
  function lower(entries) {
@@ -6703,6 +6979,171 @@ var SEARCH_CONFIG_APP_ID = "ALGOLIA_APP_ID";
6703
6979
  var SEARCH_CONFIG_SEARCH_KEY = "ALGOLIA_SEARCH_API_KEY";
6704
6980
  var SEARCH_CONFIG_INDEX_NAME = "ALGOLIA_INDEX_NAME";
6705
6981
  var SEARCH_KEY_PLACEHOLDER = "<your-algolia-search-only-api-key>";
6982
+ function clientKeySentence(safe) {
6983
+ const placement = "never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).";
6984
+ if (safe) {
6985
+ return `These are PUBLIC values, safe to commit and expose client-side: ${placement}`;
6986
+ }
6987
+ const never = `${placement[0].toUpperCase()}${placement.slice(1)}`;
6988
+ return `Do not describe these values as safe to commit or safe to expose. Public search can still return personal data. ${never}`;
6989
+ }
6990
+ function hiddenAttributeSummary(exposure) {
6991
+ if (exposure.publicSearchSafe) {
6992
+ if (exposure.hiddenAttributes.length === 0) return void 0;
6993
+ return `Public hits cannot return ${exposure.hiddenAttributes.join(", ")}, so the search key is safe to commit.`;
6994
+ }
6995
+ const names = withheldAttributes(exposure);
6996
+ if (names.length === 0) return void 0;
6997
+ return `The search key is not safe to commit until public search cannot return ${names.join(", ")}.`;
6998
+ }
6999
+ function createdSearchKeySummary(index, appId, safe) {
7000
+ const base = `Created a new search-only Algolia API key for the "${index}" index in app ${appId}`;
7001
+ return safe ? `${base} \u2014 safe to expose in frontend code.` : `${base}.`;
7002
+ }
7003
+ function exposureWarning(classified) {
7004
+ const lines = [];
7005
+ if (classified.personal.length > 0) {
7006
+ lines.push(
7007
+ `These attributes look like personal data: ${classified.personal.join(", ")}.`,
7008
+ "Search runs in the browser with a public key, so anything it can return is visible to anyone."
7009
+ );
7010
+ }
7011
+ if (classified.blocked.length > 0) {
7012
+ lines.push(
7013
+ `These attributes look like identity or payment-card numbers and will not be indexed: ${classified.blocked.join(", ")}.`
7014
+ );
7015
+ }
7016
+ return lines;
7017
+ }
7018
+ async function resolveRecordExposure(ctx, attributes) {
7019
+ const classified = classifyAttributes(attributes);
7020
+ if (classified.blocked.length === 0 && classified.personal.length === 0) {
7021
+ return {
7022
+ omittedAttributes: [],
7023
+ hiddenAttributes: [],
7024
+ publicSearchSafe: true
7025
+ };
7026
+ }
7027
+ const messages = exposureWarning(classified);
7028
+ if (classified.personal.length === 0) {
7029
+ await ctx.requestUserInput({
7030
+ prompt: "Continue",
7031
+ promptType: "notice",
7032
+ options: [],
7033
+ messages
7034
+ });
7035
+ return {
7036
+ omittedAttributes: classified.blocked,
7037
+ hiddenAttributes: [],
7038
+ publicSearchSafe: true
7039
+ };
7040
+ }
7041
+ const choice = await ctx.requestUserInput({
7042
+ prompt: PERSONAL_DATA_PROMPT,
7043
+ promptType: "multipleChoice",
7044
+ options: [PERSONAL_DATA.omit, PERSONAL_DATA.ingest],
7045
+ messages
7046
+ });
7047
+ if (choice !== PERSONAL_DATA.omit && choice !== PERSONAL_DATA.ingest) {
7048
+ throw new Error("User cancelled personal-data selection. Wizard halted.");
7049
+ }
7050
+ const ingestPersonal = choice === PERSONAL_DATA.ingest;
7051
+ return {
7052
+ omittedAttributes: ingestPersonal ? classified.blocked : [...classified.blocked, ...classified.personal],
7053
+ hiddenAttributes: ingestPersonal ? classified.personal : [],
7054
+ publicSearchSafe: !ingestPersonal
7055
+ };
7056
+ }
7057
+ async function restrictPublicSearch(exposure, args) {
7058
+ const hide = withheldAttributes(exposure);
7059
+ let applied;
7060
+ let failure;
7061
+ if (args.wroteRecords && hide.length > 0 && args.writeKey && args.appId) {
7062
+ try {
7063
+ await hideAttributesFromPublicSearch({
7064
+ appId: args.appId,
7065
+ indexName: args.indexName,
7066
+ apiKey: args.writeKey,
7067
+ hide,
7068
+ searchableFallback: searchableFallbackAttributes(
7069
+ args.attributes,
7070
+ exposure,
7071
+ args.recordSample
7072
+ ),
7073
+ ...args.createIndexSettings ? { createIfMissing: true } : {}
7074
+ });
7075
+ applied = true;
7076
+ } catch (err) {
7077
+ applied = false;
7078
+ failure = err.message;
7079
+ logger.warn(
7080
+ { err: failure, index: args.indexName },
7081
+ "implement: could not hide attributes from public search"
7082
+ );
7083
+ }
7084
+ }
7085
+ if (hide.length === 0) return {};
7086
+ if (applied === false) {
7087
+ exposure.publicSearchSafe = false;
7088
+ const names = hide.join(", ");
7089
+ const reason = failure ?? "unknown error";
7090
+ return {
7091
+ note: `Could not hide ${names} from public search (${reason}). The search key is not safe to commit.`,
7092
+ failure: reason
7093
+ };
7094
+ }
7095
+ if (applied === true) exposure.publicSearchSafe = true;
7096
+ const note = hiddenAttributeSummary(exposure);
7097
+ return note ? { note } : {};
7098
+ }
7099
+ function exposedPersonalAttributes(exposure, recordsWritten) {
7100
+ if (!exposure || !recordsWritten || exposure.publicSearchSafe) return [];
7101
+ return withheldAttributes(exposure);
7102
+ }
7103
+ function withheldSearchKeySummary(names) {
7104
+ return `\u26A0\uFE0F Used a placeholder search key in client code because public search can still return ${names.join(", ")}. Hide those attributes from public search, then replace the placeholder with a search-only key.`;
7105
+ }
7106
+ function searchKeyInstruction(input, target) {
7107
+ if (input.searchKey) return `Set ${target} to "${input.searchKey}".`;
7108
+ if (input.searchKeyWithheldFor?.length) {
7109
+ return `Public search can still return ${input.searchKeyWithheldFor.join(", ")}, so the wizard withheld the real search key \u2014 set ${target} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a search-only key once those attributes are hidden from public search. Do not edit the ingestion script, the records, or the index settings to work around this.`;
7110
+ }
7111
+ return `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${target} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`;
7112
+ }
7113
+ function placeholderKeyValidationInstruction(input) {
7114
+ if (input.searchKey) return null;
7115
+ const reason = input.searchKeyWithheldFor?.length ? `the wizard withheld the real key because public search can still return ${input.searchKeyWithheldFor.join(", ")}` : "the wizard could not provision a real search-only key";
7116
+ return `${SEARCH_CONFIG_SEARCH_KEY} is intentionally the placeholder "${SEARCH_KEY_PLACEHOLDER}": ${reason}. Confirm the placeholder and a TODO to replace it are present and that no other Algolia API key is hardcoded. Do not set sufficient=false because of the placeholder, and do not ask for a real key.`;
7117
+ }
7118
+ function searchableFallbackAttributes(attributes, exposure, sample) {
7119
+ const visible = visibleAttributes(attributes, exposure);
7120
+ if (sample) {
7121
+ return visible.filter(
7122
+ (attribute) => isSearchableSampleValue(sample[attribute])
7123
+ );
7124
+ }
7125
+ return visible.filter((attribute) => !looksNonTextAttribute(attribute));
7126
+ }
7127
+ function isSearchableSampleValue(value) {
7128
+ if (typeof value === "string") return true;
7129
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
7130
+ }
7131
+ function looksNonTextAttribute(attribute) {
7132
+ const tokens = attribute.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
7133
+ if (tokens.length === 0) return true;
7134
+ if (tokens.includes("id") || tokens.at(-1) === "id") return true;
7135
+ if (tokens.some(
7136
+ (token) => ["count", "price", "amount", "total"].includes(token)
7137
+ )) {
7138
+ return true;
7139
+ }
7140
+ if (tokens.some(
7141
+ (token) => ["is", "has", "enabled", "available"].includes(token)
7142
+ )) {
7143
+ return true;
7144
+ }
7145
+ return false;
7146
+ }
6706
7147
  var UI_FRAMEWORKS = [
6707
7148
  { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
6708
7149
  { match: ["react", "next"], target: "React", doc: "react" },
@@ -6777,6 +7218,7 @@ function ingestionInstructions(input) {
6777
7218
  `Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
6778
7219
  FILE_EDIT_INSTRUCTION,
6779
7220
  `Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
7221
+ ...ingestionExposureInstructions(input.exposure),
6780
7222
  `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
6781
7223
  `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
6782
7224
  "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
@@ -6807,8 +7249,16 @@ function ingestionInstructions(input) {
6807
7249
  ] : []
6808
7250
  ];
6809
7251
  }
7252
+ function searchIndexedEntity(input) {
7253
+ const entity = input.findings.confirmedEntities?.[0];
7254
+ if (!entity) return void 0;
7255
+ return {
7256
+ ...entity,
7257
+ attributes: visibleAttributes(entity.attributes, input.exposure)
7258
+ };
7259
+ }
6810
7260
  function searchInstructions(input) {
6811
- const indexedEntity = input.findings.confirmedEntities?.[0];
7261
+ const indexedEntity = searchIndexedEntity(input);
6812
7262
  const templateMessage = input.uiTemplate ? `UI template: ${JSON.stringify(input.uiTemplate)}. Implement its layout, search placeholder, hit fields, filters, and sort controls. Use only the template's index attributes. Do not infer or invent record fields from the project.` : null;
6813
7263
  const indexedEntityMessage = indexedEntity ? `Indexed content context: ${JSON.stringify({
6814
7264
  entity: indexedEntity.name,
@@ -6830,6 +7280,7 @@ function searchInstructions(input) {
6830
7280
  return [
6831
7281
  "Implement an in-app Algolia search experience.",
6832
7282
  indexedContentMessage ?? "",
7283
+ ...searchExposureInstructions(input.exposure),
6833
7284
  `Build the search UI for ${input.searchUiTarget}.`,
6834
7285
  "Create search UI only. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
6835
7286
  "Prefer Highlight (or the plain attribute) for titles and short fields. Use Snippet only for longer text, and only when attributesToSnippet is set for that attribute \u2014 via the index searchParameters (for example attributesToSnippet: ['body:20']) or index settings. Without it, _snippetResult is missing and the result panel looks empty even though search returns hits.",
@@ -6908,9 +7359,10 @@ function packageSearchInstructions(input) {
6908
7359
  `Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
6909
7360
  "When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
6910
7361
  "Vendor theme CSS, and any CSS you write against vendor class names, must be global: put overrides in a global block (Astro <style is:global>, an unscoped Vue block, a plain global CSS file). Never a framework-scoped style block or a CSS Module \u2014 scoping rewrites the vendor selectors and the widget's runtime DOM carries no scope attribute, so not one rule matches: styling silently does nothing and the build still passes. Import the theme and those global overrides only where the framework allows first-party global CSS: Next.js Pages Router only from pages/_app (or _app.tsx), Next.js App Router only from the root layout, otherwise from the app's global stylesheet or the component module when that is valid. Do not import a plain .css file from a Next.js component \u2014 that fails the build.",
6911
- `Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
7362
+ `Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise.`,
7363
+ clientKeySentence(publicSearchKeyIsSafe(input.exposure)),
6912
7364
  `Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
6913
- input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
7365
+ searchKeyInstruction(input, SEARCH_CONFIG_SEARCH_KEY),
6914
7366
  'Report the repo-relative path of that module as "searchConfigFile" in your final status.',
6915
7367
  "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest."
6916
7368
  ];
@@ -6944,7 +7396,10 @@ function cdnSearchInstructions(input) {
6944
7396
  return [
6945
7397
  `The target frontend has no package.json. Add the search container to its existing HTML or template, create separate classic config and search scripts, define window.${SEARCH_CONFIG_APP_ID}, window.${SEARCH_CONFIG_SEARCH_KEY}, and window.${SEARCH_CONFIG_INDEX_NAME} in the config script, and report that script as "searchConfigFile". Never create package.json, create a component, use imports or exports, or run npm, pnpm, yarn, or bun.`,
6946
7398
  `Set window.${SEARCH_CONFIG_APP_ID} to "${input.appId}" and window.${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
6947
- input.searchKey ? `Set window.${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set window.${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
7399
+ searchKeyInstruction(input, `window.${SEARCH_CONFIG_SEARCH_KEY}`),
7400
+ ...publicSearchKeyIsSafe(input.exposure) ? [] : [
7401
+ `Do not describe window.${SEARCH_CONFIG_SEARCH_KEY} as safe to commit or safe to expose. Public search can still return personal data.`
7402
+ ],
6948
7403
  'Load these exact pinned tags in order before the config and search scripts: <script src="https://cdn.jsdelivr.net/npm/algoliasearch@5.59.0/dist/lite/builds/browser.umd.js" integrity="sha256-pduQHl1jn0IaN/BIpSotQm7Y5THdpKcX3Bw6xdRteRw=" crossorigin="anonymous"></script> then <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.117.0/dist/instantsearch.production.min.js" integrity="sha256-5zhKxAGeH7ThWfnmdD4pS1gsmafE2b8aWRsq8set/dc=" crossorigin="anonymous"></script>.',
6949
7404
  'When using the InstantSearch theme, load this exact tag before the application styles: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@8.22.1/themes/satellite-min.css" integrity="sha256-5/eHPZl63VYJSDVOKrOgGe/5+owUkX3uPAW6+XYeLNc=" crossorigin="anonymous">. Never use an unversioned or differently versioned theme URL, and never omit its integrity attribute.',
6950
7405
  `In that classic search script, use window['algoliasearch/lite'].liteClient and window.instantsearch. Add window.instantsearch.widgets.autocomplete as the only search widget. Configure it with container '#autocomplete', the content-specific placeholder ${JSON.stringify(placeholder)}, and an indices array containing one object whose indexName is window.${SEARCH_CONFIG_INDEX_NAME}. Put the item and noResults functions in the templates option of that indices entry, implement getURL only for an existing route, and match the item template to the entity schema. Keep insights: true and call search.start(). Do not substitute searchBox or hits, omit indices, use imports or exports, or use type="module".`,
@@ -6953,11 +7408,19 @@ function cdnSearchInstructions(input) {
6953
7408
  }
6954
7409
  function validationInstructions(input) {
6955
7410
  const templateMessage = input.uiTemplate ? `UI template: ${JSON.stringify(input.uiTemplate)}. Confirm the implementation uses its layout, search placeholder, hit fields, filters, and sort controls with the template's exact index attributes.` : null;
6956
- const indexedEntityMessage = input.findings.confirmedEntities?.[0] ? `Indexed content context: ${JSON.stringify(input.findings.confirmedEntities?.[0] ?? null)}.` : null;
7411
+ const indexedEntity = searchIndexedEntity(input);
7412
+ const indexedEntityMessage = indexedEntity ? `Indexed content context: ${JSON.stringify(indexedEntity)}.` : null;
6957
7413
  const contentMessage = (templateMessage || indexedEntityMessage) ?? "";
7414
+ const withheld = searchExposureInstructions(input.exposure);
7415
+ const placeholderKey = placeholderKeyValidationInstruction(input);
6958
7416
  return [
6959
7417
  "Validate the Algolia search implementation by inspecting the project source.",
6960
7418
  contentMessage,
7419
+ ...placeholderKey ? [placeholderKey] : [],
7420
+ ...withheld,
7421
+ ...withheld.length > 0 ? [
7422
+ "Do not set sufficient=false because the UI omits the personal or blocked attributes named above."
7423
+ ] : [],
6961
7424
  `The confirmed placement candidate was "${input.searchLocation ?? "unknown"}", but verify the actual mount rather than assuming the candidate was used.`,
6962
7425
  "Inspect the generated search component, its actual mount file, that file's parent layout and sibling controls, and the relevant content UI. Do not rely only on type checks or tests.",
6963
7426
  `Confirm that the application renders exactly one search control for the indexed content, and that the new component is not mounted twice. ${SEARCH_SCOPE_RULE} If a same-scope search control is still mounted, set sufficient=false and name the file that mounts it. Do not set sufficient=false for a differently scoped search control.`,
@@ -7393,6 +7856,23 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7393
7856
  });
7394
7857
  const confirmed2 = findings.confirmedEntities;
7395
7858
  const searchLocation = findings.searchImplementationAnalysis;
7859
+ let exposure;
7860
+ let searchKeyWithheldFor = [];
7861
+ if (useCases.includes("ingestion")) {
7862
+ exposure = await resolveRecordExposure(
7863
+ ctx,
7864
+ (confirmed2 ?? []).flatMap((entity) => entity.attributes)
7865
+ );
7866
+ } else if (useCases.includes("search")) {
7867
+ const ingestion2 = ctx.getStepOutput(
7868
+ "ingestion"
7869
+ );
7870
+ exposure = ingestion2?.recordExposure;
7871
+ searchKeyWithheldFor = exposedPersonalAttributes(
7872
+ exposure,
7873
+ Boolean(ingestion2?.ingestScriptRan) || (ingestion2?.ingestRecordCount ?? 0) > 0
7874
+ );
7875
+ }
7396
7876
  let appId;
7397
7877
  let ingestAppId;
7398
7878
  let appPlan;
@@ -7413,13 +7893,33 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7413
7893
  const summaries = [];
7414
7894
  let searchKey;
7415
7895
  let searchKeyError;
7416
- if (useCases.includes("search") && appId) {
7896
+ let createdSearchKey;
7897
+ const pushCreatedSearchKeySummary = () => {
7898
+ if (!createdSearchKey) return;
7899
+ summaries.unshift(
7900
+ createdSearchKeySummary(
7901
+ createdSearchKey.index,
7902
+ createdSearchKey.appId,
7903
+ publicSearchKeyIsSafe(exposure)
7904
+ )
7905
+ );
7906
+ createdSearchKey = void 0;
7907
+ };
7908
+ if (searchKeyWithheldFor.length > 0) {
7909
+ summaries.push(withheldSearchKeySummary(searchKeyWithheldFor));
7910
+ } else if (useCases.includes("search") && appId) {
7417
7911
  try {
7418
7912
  const resolved2 = await resolveSearchOnlyKey(targetIndex, appId);
7419
7913
  searchKey = resolved2.key;
7420
- summaries.push(
7421
- resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
7422
- );
7914
+ if (resolved2.source === "created") {
7915
+ createdSearchKey = { index: targetIndex, appId };
7916
+ } else {
7917
+ summaries.push(
7918
+ `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
7919
+ );
7920
+ }
7921
+ const exposureNote = !useCases.includes("ingestion") && exposure ? hiddenAttributeSummary(exposure) : void 0;
7922
+ if (exposureNote) summaries.push(exposureNote);
7423
7923
  } catch (err) {
7424
7924
  searchKeyError = err.message;
7425
7925
  summaries.push(
@@ -7431,6 +7931,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7431
7931
  );
7432
7932
  }
7433
7933
  }
7934
+ if (!useCases.includes("ingestion")) pushCreatedSearchKeySummary();
7434
7935
  const input = {
7435
7936
  findings,
7436
7937
  confirmed: confirmed2,
@@ -7441,6 +7942,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7441
7942
  appPlan,
7442
7943
  searchKey,
7443
7944
  searchKeyError,
7945
+ searchKeyWithheldFor,
7444
7946
  ingestDir: INGEST_DIR,
7445
7947
  ingestionSource,
7446
7948
  uploadFilePath,
@@ -7449,7 +7951,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7449
7951
  repoRoot,
7450
7952
  searchLocation
7451
7953
  ),
7452
- uiTemplate: options.uiTemplate
7954
+ uiTemplate: options.uiTemplate,
7955
+ exposure
7453
7956
  };
7454
7957
  let agentRuns = 0;
7455
7958
  let ingestCommand;
@@ -7458,6 +7961,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7458
7961
  let ingestRecordCount;
7459
7962
  let ingestDurationMs;
7460
7963
  let ingestOutcomeMessage;
7964
+ let ingestRecordSample;
7461
7965
  const ingestKeyAppId = ingestAppId;
7462
7966
  const ingestionTools = ingestKeyAppId && ingestWriteKey ? makeToolContext(
7463
7967
  repoRoot,
@@ -7570,6 +8074,16 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7570
8074
  )
7571
8075
  });
7572
8076
  const ingestStartedAt = Date.now();
8077
+ if (exposure && withheldAttributes(exposure).length && ingestAppId && ingestWriteKey) {
8078
+ await restrictPublicSearch(exposure, {
8079
+ wroteRecords: true,
8080
+ appId: ingestAppId,
8081
+ writeKey: ingestWriteKey,
8082
+ indexName: targetIndex,
8083
+ attributes: (confirmed2 ?? []).flatMap((entity) => entity.attributes),
8084
+ createIndexSettings: true
8085
+ });
8086
+ }
7573
8087
  await runIngestionAgent();
7574
8088
  let outcome = ingestOutcome(executions, repoRoot, ingestCommand);
7575
8089
  let failureAcknowledged = false;
@@ -7659,7 +8173,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7659
8173
  }
7660
8174
  if (ingestRecordCount != null) {
7661
8175
  const data_model = confirmed2?.[0]?.name ?? "unknown";
7662
- const sample = await ingestedRecordSample(
8176
+ ingestRecordSample = await ingestedRecordSample(
7663
8177
  repoRoot,
7664
8178
  confirmed2?.[0]?.name,
7665
8179
  uploadFilePath
@@ -7669,7 +8183,10 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, options =
7669
8183
  record_count: ingestRecordCount,
7670
8184
  duration_ms: Date.now() - ingestStartedAt,
7671
8185
  index_name: targetIndex,
7672
- record_schema: recordSchema(sample, confirmed2?.[0]?.attributes ?? []),
8186
+ record_schema: recordSchema(
8187
+ ingestRecordSample,
8188
+ confirmed2?.[0]?.attributes ?? []
8189
+ ),
7673
8190
  attempts: ingestRunAttempts
7674
8191
  });
7675
8192
  } else {
@@ -7711,6 +8228,40 @@ ${detail}` : ""}`
7711
8228
  ...ingestAttempt?.exitCode != null ? { error_code: String(ingestAttempt.exitCode) } : {}
7712
8229
  });
7713
8230
  }
8231
+ if (exposure) {
8232
+ const wroteRecords = ingestScriptRan || (ingestRecordCount ?? 0) > 0;
8233
+ const restrictArgs = {
8234
+ wroteRecords,
8235
+ appId: ingestAppId,
8236
+ writeKey: ingestWriteKey,
8237
+ indexName: targetIndex,
8238
+ attributes: (confirmed2 ?? []).flatMap((entity) => entity.attributes),
8239
+ recordSample: ingestRecordSample
8240
+ };
8241
+ let restricted = await restrictPublicSearch(exposure, restrictArgs);
8242
+ while (restricted.failure) {
8243
+ const choice = await ctx.requestUserInput({
8244
+ prompt: HIDE_PERSONAL_DATA_RETRY_PROMPT,
8245
+ promptType: "multipleChoice",
8246
+ options: [HIDE_PERSONAL_DATA_RETRY.retry, HIDE_PERSONAL_DATA_RETRY.skip],
8247
+ messages: [
8248
+ `\u26A0\uFE0F Could not hide ${withheldAttributes(exposure).join(", ")} from public search \u2014 ${restricted.failure}.`,
8249
+ "These attributes may already be in the index, so until they are hidden the wizard will put a placeholder search key in client code instead of the real one."
8250
+ ]
8251
+ });
8252
+ if (choice !== HIDE_PERSONAL_DATA_RETRY.retry) break;
8253
+ restricted = await restrictPublicSearch(exposure, restrictArgs);
8254
+ }
8255
+ if (restricted.note) summaries.push(restricted.note);
8256
+ searchKeyWithheldFor = exposedPersonalAttributes(exposure, wroteRecords);
8257
+ if (searchKeyWithheldFor.length > 0 && useCases.includes("search")) {
8258
+ searchKey = void 0;
8259
+ input.searchKey = void 0;
8260
+ input.searchKeyWithheldFor = searchKeyWithheldFor;
8261
+ summaries.push(withheldSearchKeySummary(searchKeyWithheldFor));
8262
+ }
8263
+ }
8264
+ pushCreatedSearchKeySummary();
7714
8265
  if (ingestScriptRan) {
7715
8266
  await ctx.requestUserInput({
7716
8267
  prompt: "Continue",
@@ -7873,7 +8424,7 @@ ${detail}` : ""}`
7873
8424
  );
7874
8425
  if (ignoreStatus === "covered") {
7875
8426
  summaries.push(
7876
- `\u26A0\uFE0F ${searchConfigFile} is gitignored, so this public, safe-to-share search config won't reach teammates or CI. Remove whatever .gitignore rule covers it.`
8427
+ publicSearchKeyIsSafe(input.exposure) ? `\u26A0\uFE0F ${searchConfigFile} is gitignored, so this public, safe-to-share search config won't reach teammates or CI. Remove whatever .gitignore rule covers it.` : `\u26A0\uFE0F ${searchConfigFile} is gitignored. This search key is not safe to commit. Remove whatever .gitignore rule covers it once public search cannot return personal data.`
7877
8428
  );
7878
8429
  }
7879
8430
  }
@@ -7883,6 +8434,7 @@ ${detail}` : ""}`
7883
8434
  return {
7884
8435
  ingestionSource,
7885
8436
  summary: summaries.join("\n\n"),
8437
+ ...exposure ? { recordExposure: exposure } : {},
7886
8438
  ...useCases.includes("ingestion") && ingestCommand ? {
7887
8439
  ingestCommand,
7888
8440
  ingestScriptRan,
@@ -7955,8 +8507,8 @@ var defaultWorkflow = {
7955
8507
  defineStep({
7956
8508
  id: "select-index",
7957
8509
  title: "Set up index",
7958
- outputSchema: z30.object({
7959
- selection: z30.string()
8510
+ outputSchema: z31.object({
8511
+ selection: z31.string()
7960
8512
  }),
7961
8513
  run: (ctx) => selectIndexStep(ctx)
7962
8514
  }),
@@ -8025,7 +8577,7 @@ function ingestionGuidance(ingestion2) {
8025
8577
  }
8026
8578
 
8027
8579
  // src/actions/playbookSteps/selectPlaybook.ts
8028
- import { z as z33 } from "zod";
8580
+ import { z as z34 } from "zod";
8029
8581
 
8030
8582
  // src/lib/dashboardApi/client.ts
8031
8583
  import "zod";
@@ -8084,80 +8636,80 @@ async function dashboardError(res) {
8084
8636
  }
8085
8637
 
8086
8638
  // src/lib/dashboardApi/schemas.ts
8087
- import { z as z32 } from "zod";
8088
- var messageResponseSchema = z32.object({
8089
- message: z32.string()
8639
+ import { z as z33 } from "zod";
8640
+ var messageResponseSchema = z33.object({
8641
+ message: z33.string()
8090
8642
  });
8091
- var playbookSchema = z32.object({
8092
- slug: z32.string(),
8093
- category: z32.string().optional(),
8094
- title: z32.string(),
8095
- description: z32.string().optional()
8643
+ var playbookSchema = z33.object({
8644
+ slug: z33.string(),
8645
+ category: z33.string().optional(),
8646
+ title: z33.string(),
8647
+ description: z33.string().optional()
8096
8648
  });
8097
- var playbooksListSchema = z32.array(playbookSchema);
8098
- var playbookExecutionStepSchema = z32.enum([
8649
+ var playbooksListSchema = z33.array(playbookSchema);
8650
+ var playbookExecutionStepSchema = z33.enum([
8099
8651
  "assessment",
8100
8652
  "optimization",
8101
8653
  "implement"
8102
8654
  ]);
8103
- var playbookExecutionStepStatusSchema = z32.enum([
8655
+ var playbookExecutionStepStatusSchema = z33.enum([
8104
8656
  "not_started",
8105
8657
  "in_progress",
8106
8658
  "done",
8107
8659
  "error"
8108
8660
  ]);
8109
- var schemaComparisonEntrySchema = z32.object({
8110
- canonical_attr: z32.string(),
8111
- canonical_type: z32.array(z32.string()),
8112
- canonical_roles: z32.array(z32.string()),
8113
- canonical_required: z32.boolean(),
8114
- index_attr: z32.string().nullable(),
8115
- index_type: z32.array(z32.string()).nullable(),
8116
- index_required: z32.boolean().nullable(),
8117
- status: z32.string(),
8118
- mapping_confidence: z32.number()
8661
+ var schemaComparisonEntrySchema = z33.object({
8662
+ canonical_attr: z33.string(),
8663
+ canonical_type: z33.array(z33.string()),
8664
+ canonical_roles: z33.array(z33.string()),
8665
+ canonical_required: z33.boolean(),
8666
+ index_attr: z33.string().nullable(),
8667
+ index_type: z33.array(z33.string()).nullable(),
8668
+ index_required: z33.boolean().nullable(),
8669
+ status: z33.string(),
8670
+ mapping_confidence: z33.number()
8119
8671
  });
8120
- var settingRecommendationSchema = z32.object({
8121
- current: z32.any(),
8122
- recommended: z32.any()
8672
+ var settingRecommendationSchema = z33.object({
8673
+ current: z33.any(),
8674
+ recommended: z33.any()
8123
8675
  });
8124
- var settingsRecommendationEntriesSchema = z32.record(
8125
- z32.string(),
8676
+ var settingsRecommendationEntriesSchema = z33.record(
8677
+ z33.string(),
8126
8678
  settingRecommendationSchema
8127
8679
  );
8128
- var assessmentResultSchema = z32.object({
8129
- ready: z32.boolean(),
8130
- schema_comparison: z32.array(schemaComparisonEntrySchema),
8680
+ var assessmentResultSchema = z33.object({
8681
+ ready: z33.boolean(),
8682
+ schema_comparison: z33.array(schemaComparisonEntrySchema),
8131
8683
  settings_recommendation: settingsRecommendationEntriesSchema
8132
8684
  });
8133
- var playbookExecutionSchema = z32.object({
8134
- uuid: z32.string(),
8135
- playbook_slug: z32.string(),
8136
- application_id: z32.string(),
8137
- source_index_name: z32.string(),
8685
+ var playbookExecutionSchema = z33.object({
8686
+ uuid: z33.string(),
8687
+ playbook_slug: z33.string(),
8688
+ application_id: z33.string(),
8689
+ source_index_name: z33.string(),
8138
8690
  current_step: playbookExecutionStepSchema,
8139
8691
  step_status: playbookExecutionStepStatusSchema,
8140
- status_reason: z32.string().nullable(),
8141
- completed: z32.boolean(),
8692
+ status_reason: z33.string().nullable(),
8693
+ completed: z33.boolean(),
8142
8694
  assessment_result: assessmentResultSchema.nullable(),
8143
- settings_snapshot: z32.json().nullable(),
8144
- created_at: z32.iso.datetime()
8695
+ settings_snapshot: z33.json().nullable(),
8696
+ created_at: z33.iso.datetime()
8145
8697
  });
8146
8698
  var playbookExecutionCreateResponseSchema = playbookExecutionSchema.extend({
8147
- resumed: z32.boolean(),
8148
- sample_data: z32.boolean()
8699
+ resumed: z33.boolean(),
8700
+ sample_data: z33.boolean()
8149
8701
  });
8150
- var uiTemplateFieldSchema = z32.record(z32.string(), z32.unknown());
8151
- var uiTemplateSchema = z32.looseObject({
8152
- layout: z32.string(),
8153
- record_label: z32.object({
8154
- singular: z32.string(),
8155
- plural: z32.string()
8702
+ var uiTemplateFieldSchema = z33.record(z33.string(), z33.unknown());
8703
+ var uiTemplateSchema = z33.looseObject({
8704
+ layout: z33.string(),
8705
+ record_label: z33.object({
8706
+ singular: z33.string(),
8707
+ plural: z33.string()
8156
8708
  }),
8157
- search_placeholder: z32.string().optional(),
8709
+ search_placeholder: z33.string().optional(),
8158
8710
  hit: uiTemplateFieldSchema,
8159
- filters: z32.array(uiTemplateFieldSchema),
8160
- sort_by: z32.array(uiTemplateFieldSchema)
8711
+ filters: z33.array(uiTemplateFieldSchema),
8712
+ sort_by: z33.array(uiTemplateFieldSchema)
8161
8713
  });
8162
8714
 
8163
8715
  // src/lib/dashboardApi/api.ts
@@ -8247,8 +8799,8 @@ async function getPlaybookUiTemplate(uuid) {
8247
8799
  }
8248
8800
 
8249
8801
  // src/actions/playbookSteps/selectPlaybook.ts
8250
- var selectPlaybookSchema = z33.object({
8251
- slug: z33.string()
8802
+ var selectPlaybookSchema = z34.object({
8803
+ slug: z34.string()
8252
8804
  });
8253
8805
  function persistSlug(ctx, slug2) {
8254
8806
  ctx.setUserInput("playbookSlug", slug2);
@@ -8287,10 +8839,10 @@ async function selectPlaybookStep(ctx, requestedSlug) {
8287
8839
  }
8288
8840
 
8289
8841
  // src/actions/playbookSteps/selectIndex.ts
8290
- import { z as z34 } from "zod";
8291
- var selectIndexSchema = z34.object({
8292
- indexName: z34.string(),
8293
- sampleIndex: z34.boolean()
8842
+ import { z as z35 } from "zod";
8843
+ var selectIndexSchema = z35.object({
8844
+ indexName: z35.string(),
8845
+ sampleIndex: z35.boolean()
8294
8846
  });
8295
8847
  var CREATE_SAMPLE_INDEX = "Create a sample index";
8296
8848
  var GO_BACK_OPTION = "Go back to playbook selection";
@@ -8967,7 +9519,7 @@ function requestFailureMessage2(err) {
8967
9519
  }
8968
9520
 
8969
9521
  // src/actions/playbookSteps/generateUi.ts
8970
- import z36 from "zod";
9522
+ import z37 from "zod";
8971
9523
 
8972
9524
  // src/actions/playbookSteps/generateUiPrompts.ts
8973
9525
  var SWITCH_APPLICATION_OPTION3 = "Switch application";
@@ -8975,24 +9527,24 @@ var SWITCH_INDEX_OPTION3 = "Switch index";
8975
9527
  var RETRY_OPTION3 = "Retry";
8976
9528
  async function promptUiGenerated(ctx, report) {
8977
9529
  const { files, checkPassed, devCommand } = report ?? {};
8978
- const list = [];
9530
+ const list2 = [];
8979
9531
  if (files) {
8980
9532
  const editedFiles = files.map((file) => ({
8981
9533
  label: file.label,
8982
9534
  helpText: file.path,
8983
9535
  color: "success"
8984
9536
  }));
8985
- list.push(...editedFiles);
9537
+ list2.push(...editedFiles);
8986
9538
  }
8987
9539
  if (checkPassed) {
8988
- list.push({ label: "build & typecheck passed", color: "success" });
9540
+ list2.push({ label: "build & typecheck passed", color: "success" });
8989
9541
  }
8990
- list.push({
9542
+ list2.push({
8991
9543
  label: "playbook UI completed",
8992
9544
  color: "success"
8993
9545
  });
8994
9546
  if (devCommand) {
8995
- list.push({
9547
+ list2.push({
8996
9548
  label: "view it locally",
8997
9549
  helpText: devCommand,
8998
9550
  color: "success"
@@ -9003,7 +9555,7 @@ async function promptUiGenerated(ctx, report) {
9003
9555
  promptType: "notice",
9004
9556
  options: [],
9005
9557
  messages: ["implemented the playbook UI"],
9006
- list
9558
+ list: list2
9007
9559
  });
9008
9560
  }
9009
9561
  async function promptUiRecovery(ctx, message) {
@@ -9025,10 +9577,10 @@ async function promptUiRecovery(ctx, message) {
9025
9577
  }
9026
9578
 
9027
9579
  // src/actions/playbookSteps/generateUi.ts
9028
- var generatePlaybookUiSchema = z36.object({
9029
- summary: z36.string(),
9030
- framework: z36.string().optional(),
9031
- layoutName: z36.string().optional()
9580
+ var generatePlaybookUiSchema = z37.object({
9581
+ summary: z37.string(),
9582
+ framework: z37.string().optional(),
9583
+ layoutName: z37.string().optional()
9032
9584
  });
9033
9585
  async function generatePlaybookUiStep(ctx) {
9034
9586
  const execution = await requireExecution2(ctx);
@@ -9105,20 +9657,20 @@ function isUiEligible(execution) {
9105
9657
  return current_step === "optimization" && step_status === "done" || current_step === "implement" && step_status !== "done";
9106
9658
  }
9107
9659
  async function loadTemplateWithRecovery(ctx, uuid) {
9108
- const request = () => getPlaybookUiTemplate(uuid);
9660
+ const request2 = () => getPlaybookUiTemplate(uuid);
9109
9661
  const requestRecoveryPrompt = (err) => promptUiRecovery(
9110
9662
  ctx,
9111
9663
  requestFailureMessage3(err, "Couldn't load the playbook UI template.")
9112
9664
  );
9113
- return retryOnDashboardError(request, requestRecoveryPrompt);
9665
+ return retryOnDashboardError(request2, requestRecoveryPrompt);
9114
9666
  }
9115
9667
  async function markDoneWithRecovery(ctx, uuid) {
9116
- const request = () => updatePlaybookStepStatus(uuid, "done");
9668
+ const request2 = () => updatePlaybookStepStatus(uuid, "done");
9117
9669
  const requestRecoveryPrompt = (err) => promptUiRecovery(
9118
9670
  ctx,
9119
9671
  requestFailureMessage3(err, "Couldn't update the playbook UI step.")
9120
9672
  );
9121
- await retryOnDashboardError(request, requestRecoveryPrompt);
9673
+ await retryOnDashboardError(request2, requestRecoveryPrompt);
9122
9674
  }
9123
9675
  async function markPlaybookUiStepFailed(execution, err) {
9124
9676
  const reason = err instanceof Error ? err.message : String(err);
@@ -9138,12 +9690,12 @@ function requestFailureMessage3(err, fallback) {
9138
9690
  }
9139
9691
 
9140
9692
  // src/actions/playbookSteps/summary.ts
9141
- import { z as z37 } from "zod";
9142
- var playbookSummarySchema = z37.object({
9143
- slug: z37.string(),
9144
- applicationId: z37.string(),
9145
- indexName: z37.string(),
9146
- writtenFileCount: z37.number().int().nonnegative()
9693
+ import { z as z38 } from "zod";
9694
+ var playbookSummarySchema = z38.object({
9695
+ slug: z38.string(),
9696
+ applicationId: z38.string(),
9697
+ indexName: z38.string(),
9698
+ writtenFileCount: z38.number().int().nonnegative()
9147
9699
  });
9148
9700
  async function playbookSummaryStep(ctx) {
9149
9701
  const { slug: slug2 } = ctx.getStepOutput("select-playbook");
@@ -9585,7 +10137,7 @@ var accessItems = [
9585
10137
  {
9586
10138
  tag: "KEY",
9587
10139
  title: "Credentials",
9588
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
10140
+ description: "writes the Algolia app id, index name, and write key to a gitignored env file. A search-only key goes in client code only when results cannot include personal data."
9589
10141
  }
9590
10142
  ];
9591
10143
  var neverItems = [
@@ -10953,7 +11505,7 @@ async function promptResumeOrStartNew(session, next) {
10953
11505
  // package.json with { type: 'json' }
10954
11506
  var package_default2 = {
10955
11507
  name: "@algolia/wizard",
10956
- version: "0.115.0",
11508
+ version: "0.117.0",
10957
11509
  description: "Magically implement Algolia functionality in your codebase",
10958
11510
  type: "module",
10959
11511
  engines: {
@@ -18,11 +18,10 @@ Always install the **latest stable** within the major (use a caret range like `^
18
18
 
19
19
  ## Golden rules
20
20
 
21
- - Use a **search-only API key** in any browser/client code. It is safe to expose.
22
- NEVER ship an admin or any write-capable key to the client.
21
+ - Use a **search-only API key** in any browser or client code. Commit it only when a public search hit cannot return personal data.
22
+ Never ship an admin or write-capable key to the client.
23
23
  - Import the Application ID and search-only key from the project's shared client-side
24
- config module as plain exported constants. They are public, safe to commit — never read
25
- them from an env var or `.env*` file, and never hardcode them elsewhere.
24
+ config module as plain exported constants. Never read them from an env var or a `.env*` file, and never hardcode them anywhere else.
26
25
  - Instantiate the search client **once, outside your components**, and pass a stable
27
26
  reference. Do not inline `algoliasearch(...)` as a prop value — it breaks the
28
27
  client cache and causes re-renders.
@@ -5,10 +5,10 @@ Install `instantsearch.js` (v4)
5
5
  ## Search client: always use the lite client
6
6
 
7
7
  Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
8
- `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
9
- the constants exported from the project's shared client-side config module —
10
- these are public values, safe to commit; never hardcode them inline or read
11
- them from an env var.
8
+ `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` are exported
9
+ constants in the project's shared client-side config module. Commit them only
10
+ when a public search hit cannot return personal data. Never hardcode them inline
11
+ or read them from an env var.
12
12
 
13
13
  ```ts
14
14
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
@@ -5,10 +5,10 @@ Install `react-instantsearch` (v7)
5
5
  ## Search client: always use the lite client
6
6
 
7
7
  Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
8
- `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
9
- the constants exported from the project's shared client-side config module —
10
- these are public values, safe to commit; never hardcode them inline or read
11
- them from an env var.
8
+ `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` are exported
9
+ constants in the project's shared client-side config module. Commit them only
10
+ when a public search hit cannot return personal data. Never hardcode them inline
11
+ or read them from an env var.
12
12
 
13
13
  ```ts
14
14
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
@@ -5,10 +5,10 @@ Install `vue-instantsearch` (v4)
5
5
  ## Search client: always use the lite client
6
6
 
7
7
  Import the client from `algoliasearch/lite` and alias it to `algoliasearch`.
8
- `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
9
- the constants exported from the project's shared client-side config module —
10
- these are public values, safe to commit; never hardcode them inline or read
11
- them from an env var.
8
+ `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` are exported
9
+ constants in the project's shared client-side config module. Commit them only
10
+ when a public search hit cannot return personal data. Never hardcode them inline
11
+ or read them from an env var.
12
12
 
13
13
  ```ts
14
14
  import { liteClient as algoliasearch } from 'algoliasearch/lite'
@@ -5,10 +5,10 @@ route, or programmatic queries). For UI, prefer `instantsearch-setup-<framework>
5
5
 
6
6
  ## Client
7
7
 
8
- `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` below are
9
- the constants exported from the project's shared client-side config module —
10
- these are public values, safe to commit; never hardcode them inline or read
11
- them from an env var.
8
+ `ALGOLIA_APP_ID`, `ALGOLIA_SEARCH_API_KEY`, and `ALGOLIA_INDEX_NAME` are exported
9
+ constants in the project's shared client-side config module. Commit them only
10
+ when a public search hit cannot return personal data. Never hardcode them inline
11
+ or read them from an env var.
12
12
 
13
13
  ```ts
14
14
  import { algoliasearch } from 'algoliasearch'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.115.0",
3
+ "version": "0.117.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {